@ryuu-reinzz/baileys 3.5.0 → 5.0.0
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 +30 -25
- package/WAProto/fix-imports.js +22 -18
- package/WAProto/index.js +22 -18
- package/lib/Defaults/index.js +10 -9
- package/lib/Signal/libsignal.js +46 -19
- package/lib/Signal/lid-mapping.js +6 -0
- package/lib/Socket/chats.js +241 -39
- package/lib/Socket/groups.js +20 -0
- package/lib/Socket/messages-recv.js +736 -314
- package/lib/Socket/messages-send.js +279 -129
- package/lib/Socket/newsletter.js +2 -2
- package/lib/Socket/socket.js +56 -25
- package/lib/Types/{Newsletter.js → Mex.js} +9 -3
- package/lib/Types/State.js +43 -0
- package/lib/Types/index.js +1 -1
- package/lib/Utils/auth-utils.js +12 -0
- package/lib/Utils/chat-utils.js +80 -20
- package/lib/Utils/companion-reg-client-utils.js +35 -0
- package/lib/Utils/decode-wa-message.js +34 -0
- package/lib/Utils/event-buffer.js +49 -1
- package/lib/Utils/generics.js +12 -3
- package/lib/Utils/history.js +12 -9
- package/lib/Utils/identity-change-handler.js +1 -0
- package/lib/Utils/index.js +3 -1
- package/lib/Utils/link-preview.js +2 -2
- package/lib/Utils/message-retry-manager.js +40 -0
- package/lib/Utils/messages-media.js +21 -7
- package/lib/Utils/messages.js +28 -5
- package/lib/Utils/offline-node-processor.js +40 -0
- package/lib/Utils/process-message.js +103 -1
- package/lib/Utils/signal.js +42 -0
- package/lib/Utils/stanza-ack.js +38 -0
- package/lib/Utils/sync-action-utils.js +1 -0
- package/lib/Utils/tc-token-utils.js +149 -4
- package/lib/Utils/validate-connection.js +3 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +26 -3
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
- package/lib/WAUSync/Protocols/index.js +1 -0
- package/lib/WAUSync/USyncQuery.js +6 -2
- package/lib/WAUSync/USyncUser.js +8 -0
- package/package.json +39 -12
|
@@ -169,7 +169,27 @@ export const makeEventBuffer = (logger) => {
|
|
|
169
169
|
},
|
|
170
170
|
on: (...args) => ev.on(...args),
|
|
171
171
|
off: (...args) => ev.off(...args),
|
|
172
|
-
removeAllListeners: (...args) => ev.removeAllListeners(...args)
|
|
172
|
+
removeAllListeners: (...args) => ev.removeAllListeners(...args),
|
|
173
|
+
destroy() {
|
|
174
|
+
// Clear buffer timeout
|
|
175
|
+
if (bufferTimeout) {
|
|
176
|
+
clearTimeout(bufferTimeout);
|
|
177
|
+
bufferTimeout = null;
|
|
178
|
+
}
|
|
179
|
+
if (flushPendingTimeout) {
|
|
180
|
+
clearTimeout(flushPendingTimeout);
|
|
181
|
+
flushPendingTimeout = null;
|
|
182
|
+
}
|
|
183
|
+
// Clear history cache
|
|
184
|
+
historyCache.clear();
|
|
185
|
+
// Reset buffer data
|
|
186
|
+
data = makeBufferData();
|
|
187
|
+
isBuffering = false;
|
|
188
|
+
bufferCount = 0;
|
|
189
|
+
// Remove all listeners
|
|
190
|
+
ev.removeAllListeners();
|
|
191
|
+
logger.debug('Event buffer destroyed');
|
|
192
|
+
}
|
|
173
193
|
};
|
|
174
194
|
};
|
|
175
195
|
const makeBufferData = () => {
|
|
@@ -235,7 +255,33 @@ eventData, logger) {
|
|
|
235
255
|
}
|
|
236
256
|
data.historySets.empty = false;
|
|
237
257
|
data.historySets.syncType = eventData.syncType;
|
|
258
|
+
if (eventData.pastParticipants?.length) {
|
|
259
|
+
const merged = new Map();
|
|
260
|
+
const sigOf = (p) => `${p.userJid || ''}:${p.leaveTs || ''}:${p.leaveReason || ''}`;
|
|
261
|
+
const ingest = (entry) => {
|
|
262
|
+
const key = entry.groupJid ?? JSON.stringify(entry);
|
|
263
|
+
const existing = merged.get(key);
|
|
264
|
+
if (!existing) {
|
|
265
|
+
merged.set(key, { ...entry, pastParticipants: [...(entry.pastParticipants || [])] });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const seen = new Set((existing.pastParticipants || []).map(sigOf));
|
|
269
|
+
for (const p of entry.pastParticipants || []) {
|
|
270
|
+
const sig = sigOf(p);
|
|
271
|
+
if (!seen.has(sig)) {
|
|
272
|
+
existing.pastParticipants.push(p);
|
|
273
|
+
seen.add(sig);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
for (const entry of data.historySets.pastParticipants || [])
|
|
278
|
+
ingest(entry);
|
|
279
|
+
for (const entry of eventData.pastParticipants)
|
|
280
|
+
ingest(entry);
|
|
281
|
+
data.historySets.pastParticipants = [...merged.values()];
|
|
282
|
+
}
|
|
238
283
|
data.historySets.progress = eventData.progress;
|
|
284
|
+
data.historySets.chunkOrder = eventData.chunkOrder;
|
|
239
285
|
data.historySets.peerDataRequestSessionId = eventData.peerDataRequestSessionId;
|
|
240
286
|
data.historySets.isLatest = eventData.isLatest || data.historySets.isLatest;
|
|
241
287
|
break;
|
|
@@ -500,9 +546,11 @@ function consolidateEvents(data) {
|
|
|
500
546
|
chats: Object.values(data.historySets.chats),
|
|
501
547
|
messages: Object.values(data.historySets.messages),
|
|
502
548
|
contacts: Object.values(data.historySets.contacts),
|
|
549
|
+
pastParticipants: data.historySets.pastParticipants,
|
|
503
550
|
syncType: data.historySets.syncType,
|
|
504
551
|
progress: data.historySets.progress,
|
|
505
552
|
isLatest: data.historySets.isLatest,
|
|
553
|
+
chunkOrder: data.historySets.chunkOrder,
|
|
506
554
|
peerDataRequestSessionId: data.historySets.peerDataRequestSessionId
|
|
507
555
|
};
|
|
508
556
|
}
|
package/lib/Utils/generics.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Boom } from '@hapi/boom';
|
|
2
2
|
import { createHash, randomBytes } from 'crypto';
|
|
3
3
|
import { proto } from '../../WAProto/index.js';
|
|
4
|
-
const baileysVersion = [2, 3000,
|
|
4
|
+
const baileysVersion = [2, 3000, 1035194821];
|
|
5
5
|
import { DisconnectReason } from '../Types/index.js';
|
|
6
6
|
import { getAllBinaryNodeChildren, jidDecode } from '../WABinary/index.js';
|
|
7
7
|
import { sha256 } from './crypto.js';
|
|
@@ -144,10 +144,10 @@ export const generateMessageIDV2 = (userId) => {
|
|
|
144
144
|
const random = randomBytes(16);
|
|
145
145
|
random.copy(data, 28);
|
|
146
146
|
const hash = createHash('sha256').update(data).digest();
|
|
147
|
-
return '3EB0' + hash.toString('hex').toUpperCase().substring(0,
|
|
147
|
+
return '3EB0' + hash.toString('hex').toUpperCase().substring(0, 18);
|
|
148
148
|
};
|
|
149
149
|
// generate a random ID to attach to a message
|
|
150
|
-
export const generateMessageID = () => '3EB0' + randomBytes(
|
|
150
|
+
export const generateMessageID = () => '3EB0' + randomBytes(18).toString('hex').toUpperCase();
|
|
151
151
|
export function bindWaitForEvent(ev, event) {
|
|
152
152
|
return async (check, timeoutMs) => {
|
|
153
153
|
let listener;
|
|
@@ -314,6 +314,15 @@ export const getCallStatusFromNode = ({ tag, attrs }) => {
|
|
|
314
314
|
status = 'terminate';
|
|
315
315
|
}
|
|
316
316
|
break;
|
|
317
|
+
case 'preaccept':
|
|
318
|
+
status = 'preaccept';
|
|
319
|
+
break;
|
|
320
|
+
case 'transport':
|
|
321
|
+
status = 'transport';
|
|
322
|
+
break;
|
|
323
|
+
case 'relaylatency':
|
|
324
|
+
status = 'relaylatency';
|
|
325
|
+
break;
|
|
317
326
|
case 'reject':
|
|
318
327
|
status = 'reject';
|
|
319
328
|
break;
|
package/lib/Utils/history.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { pipeline } from 'stream/promises';
|
|
1
2
|
import { promisify } from 'util';
|
|
2
|
-
import { inflate } from 'zlib';
|
|
3
|
+
import { createInflate, inflate } from 'zlib';
|
|
3
4
|
import { proto } from '../../WAProto/index.js';
|
|
4
5
|
import { WAMessageStubType } from '../Types/index.js';
|
|
5
6
|
import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser } from '../WABinary/index.js';
|
|
@@ -24,13 +25,13 @@ const extractPnFromMessages = (messages) => {
|
|
|
24
25
|
};
|
|
25
26
|
export const downloadHistory = async (msg, options) => {
|
|
26
27
|
const stream = await downloadContentFromMessage(msg, 'md-msg-hist', { options });
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
buffer =
|
|
28
|
+
// Pipe decrypted stream directly through zlib inflate
|
|
29
|
+
// This avoids allocating an intermediate buffer for the compressed data
|
|
30
|
+
const inflater = createInflate();
|
|
31
|
+
const chunks = [];
|
|
32
|
+
inflater.on('data', (chunk) => chunks.push(chunk));
|
|
33
|
+
await pipeline(stream, inflater);
|
|
34
|
+
const buffer = Buffer.concat(chunks);
|
|
34
35
|
const syncData = proto.HistorySync.decode(buffer);
|
|
35
36
|
return syncData;
|
|
36
37
|
};
|
|
@@ -55,6 +56,7 @@ export const processHistoryMessage = (item, logger) => {
|
|
|
55
56
|
contacts.push({
|
|
56
57
|
id: chat.id,
|
|
57
58
|
name: chat.displayName || chat.name || chat.username || undefined,
|
|
59
|
+
username: chat.username || undefined,
|
|
58
60
|
lid: chat.lidJid || chat.accountLid || undefined,
|
|
59
61
|
phoneNumber: chat.pnJid || undefined
|
|
60
62
|
});
|
|
@@ -95,7 +97,7 @@ export const processHistoryMessage = (item, logger) => {
|
|
|
95
97
|
});
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
|
-
chats.push(
|
|
100
|
+
chats.push(chat);
|
|
99
101
|
}
|
|
100
102
|
break;
|
|
101
103
|
case proto.HistorySync.HistorySyncType.PUSH_NAME:
|
|
@@ -109,6 +111,7 @@ export const processHistoryMessage = (item, logger) => {
|
|
|
109
111
|
contacts,
|
|
110
112
|
messages,
|
|
111
113
|
lidPnMappings,
|
|
114
|
+
pastParticipants: item.pastParticipants,
|
|
112
115
|
syncType: item.syncType,
|
|
113
116
|
progress: item.progress
|
|
114
117
|
};
|
|
@@ -37,6 +37,7 @@ export async function handleIdentityChange(node, ctx) {
|
|
|
37
37
|
ctx.logger.debug({ jid: from }, 'skipping session refresh during offline processing');
|
|
38
38
|
return { action: 'skipped_offline' };
|
|
39
39
|
}
|
|
40
|
+
ctx.onBeforeSessionRefresh?.(from);
|
|
40
41
|
try {
|
|
41
42
|
await ctx.assertSessions([from], true);
|
|
42
43
|
return { action: 'session_refreshed' };
|
package/lib/Utils/index.js
CHANGED
|
@@ -11,11 +11,13 @@ export * from './chat-utils.js';
|
|
|
11
11
|
export * from './lt-hash.js';
|
|
12
12
|
export * from './auth-utils.js';
|
|
13
13
|
export * from './use-multi-file-auth-state.js';
|
|
14
|
+
export * from './use-sqlite-auth-state.js';
|
|
14
15
|
export * from './link-preview.js';
|
|
15
16
|
export * from './event-buffer.js';
|
|
16
17
|
export * from './process-message.js';
|
|
17
18
|
export * from './message-retry-manager.js';
|
|
18
19
|
export * from './browser-utils.js';
|
|
20
|
+
export * from './companion-reg-client-utils.js';
|
|
19
21
|
export * from './identity-change-handler.js';
|
|
20
|
-
export * from './
|
|
22
|
+
export * from './stanza-ack.js';
|
|
21
23
|
//# sourceMappingURL=index.js.map
|
|
@@ -19,7 +19,7 @@ export const getUrlInfo = async (text, opts = {
|
|
|
19
19
|
}) => {
|
|
20
20
|
try {
|
|
21
21
|
// retries
|
|
22
|
-
|
|
22
|
+
let retries = 0;
|
|
23
23
|
const maxRetry = 5;
|
|
24
24
|
const { getLinkPreview } = await import('link-preview-js');
|
|
25
25
|
let previewLink = text;
|
|
@@ -38,7 +38,7 @@ export const getUrlInfo = async (text, opts = {
|
|
|
38
38
|
if (forwardedURLObj.hostname === urlObj.hostname ||
|
|
39
39
|
forwardedURLObj.hostname === 'www.' + urlObj.hostname ||
|
|
40
40
|
'www.' + forwardedURLObj.hostname === urlObj.hostname) {
|
|
41
|
-
retries
|
|
41
|
+
retries += 1;
|
|
42
42
|
return true;
|
|
43
43
|
}
|
|
44
44
|
else {
|
|
@@ -52,6 +52,11 @@ export class MessageRetryManager {
|
|
|
52
52
|
ttlAutopurge: true,
|
|
53
53
|
updateAgeOnGet: true
|
|
54
54
|
}); // 15 minutes TTL
|
|
55
|
+
this.baseKeys = new LRUCache({
|
|
56
|
+
max: 1024,
|
|
57
|
+
ttl: 15 * 60 * 1000,
|
|
58
|
+
ttlAutopurge: true
|
|
59
|
+
});
|
|
55
60
|
this.pendingPhoneRequests = {};
|
|
56
61
|
this.maxMsgRetryCount = 5;
|
|
57
62
|
this.statistics = {
|
|
@@ -210,6 +215,41 @@ export class MessageRetryManager {
|
|
|
210
215
|
this.logger.debug(`Cancelled pending phone request for message ${messageId}`);
|
|
211
216
|
}
|
|
212
217
|
}
|
|
218
|
+
clear() {
|
|
219
|
+
this.recentMessagesMap.clear();
|
|
220
|
+
this.messageKeyIndex.clear();
|
|
221
|
+
this.sessionRecreateHistory.clear();
|
|
222
|
+
this.retryCounters.clear();
|
|
223
|
+
this.baseKeys.clear();
|
|
224
|
+
for (const messageId of Object.keys(this.pendingPhoneRequests)) {
|
|
225
|
+
this.cancelPendingPhoneRequest(messageId);
|
|
226
|
+
}
|
|
227
|
+
this.statistics = {
|
|
228
|
+
totalRetries: 0,
|
|
229
|
+
successfulRetries: 0,
|
|
230
|
+
failedRetries: 0,
|
|
231
|
+
mediaRetries: 0,
|
|
232
|
+
sessionRecreations: 0,
|
|
233
|
+
phoneRequests: 0
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
saveBaseKey(addr, msgId, baseKey) {
|
|
237
|
+
this.baseKeys.set(`${addr}:${msgId}`, baseKey);
|
|
238
|
+
}
|
|
239
|
+
hasSameBaseKey(addr, msgId, baseKey) {
|
|
240
|
+
const stored = this.baseKeys.get(`${addr}:${msgId}`);
|
|
241
|
+
if (!stored || stored.length !== baseKey.length) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
for (let i = 0; i < stored.length; i++) {
|
|
245
|
+
if (stored[i] !== baseKey[i])
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
deleteBaseKey(addr, msgId) {
|
|
251
|
+
this.baseKeys.delete(`${addr}:${msgId}`);
|
|
252
|
+
}
|
|
213
253
|
keyToString(key) {
|
|
214
254
|
return `${key.to}${MESSAGE_KEY_SEPARATOR}${key.id}`;
|
|
215
255
|
}
|
|
@@ -217,7 +217,7 @@ export async function getAudioWaveform(buffer, logger) {
|
|
|
217
217
|
const blockStart = blockSize * i; // the location of the first sample in the block
|
|
218
218
|
let sum = 0;
|
|
219
219
|
for (let j = 0; j < blockSize; j++) {
|
|
220
|
-
sum = sum + Math.abs(rawData[blockStart + j]
|
|
220
|
+
sum = sum + Math.abs(rawData[blockStart + j]); // find the sum of all the samples in the block
|
|
221
221
|
}
|
|
222
222
|
filteredData.push(sum / blockSize); // divide the sum by the block size to get the average
|
|
223
223
|
}
|
|
@@ -397,15 +397,27 @@ export const encryptedStream = async (media, mediaType, { logger, saveOriginalFi
|
|
|
397
397
|
throw error;
|
|
398
398
|
}
|
|
399
399
|
};
|
|
400
|
-
const
|
|
400
|
+
export const DEF_MEDIA_HOST = 'mmg.whatsapp.net';
|
|
401
401
|
const AES_CHUNK_SIZE = 16;
|
|
402
402
|
const toSmallestChunkSize = (num) => {
|
|
403
403
|
return Math.floor(num / AES_CHUNK_SIZE) * AES_CHUNK_SIZE;
|
|
404
404
|
};
|
|
405
|
-
export const getUrlFromDirectPath = (directPath) => `https://${
|
|
405
|
+
export const getUrlFromDirectPath = (directPath, host = DEF_MEDIA_HOST) => `https://${host}${directPath}`;
|
|
406
|
+
const extractHost = (url) => {
|
|
407
|
+
if (!url)
|
|
408
|
+
return undefined;
|
|
409
|
+
try {
|
|
410
|
+
return new URL(url).host;
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
406
416
|
export const downloadContentFromMessage = async ({ mediaKey, directPath, url }, type, opts = {}) => {
|
|
407
|
-
|
|
408
|
-
|
|
417
|
+
// Fallback host: explicit opt > host parsed from `url` > DEF_MEDIA_HOST.
|
|
418
|
+
// Lets us honor a non-default host carried by the proto without forcing callers to thread it through.
|
|
419
|
+
const fallbackHost = opts.host ?? extractHost(url);
|
|
420
|
+
const downloadUrl = directPath ? getUrlFromDirectPath(directPath, fallbackHost) : url;
|
|
409
421
|
if (!downloadUrl) {
|
|
410
422
|
throw new Boom('No valid media URL or directPath present in message', { statusCode: 400 });
|
|
411
423
|
}
|
|
@@ -465,7 +477,7 @@ export const downloadEncryptedContent = async (downloadUrl, { cipherKey, iv }, {
|
|
|
465
477
|
};
|
|
466
478
|
const output = new Transform({
|
|
467
479
|
transform(chunk, _, callback) {
|
|
468
|
-
let data = Buffer.concat([remainingBytes, chunk]);
|
|
480
|
+
let data = remainingBytes.length ? Buffer.concat([remainingBytes, chunk]) : chunk;
|
|
469
481
|
const decryptLength = toSmallestChunkSize(data.length);
|
|
470
482
|
remainingBytes = data.slice(decryptLength);
|
|
471
483
|
data = data.slice(0, decryptLength);
|
|
@@ -584,8 +596,10 @@ const uploadWithFetch = async ({ url, filePath, headers, timeoutMs, agent }) =>
|
|
|
584
596
|
// Convert Node.js Readable to Web ReadableStream
|
|
585
597
|
const nodeStream = createReadStream(filePath);
|
|
586
598
|
const webStream = Readable.toWeb(nodeStream);
|
|
599
|
+
// Native fetch only accepts Undici-style dispatchers, not generic https Agents.
|
|
600
|
+
const dispatcher = typeof agent?.dispatch === 'function' ? agent : undefined;
|
|
587
601
|
const response = await fetch(url, {
|
|
588
|
-
dispatcher:
|
|
602
|
+
...(dispatcher ? { dispatcher } : {}),
|
|
589
603
|
method: 'POST',
|
|
590
604
|
body: webStream,
|
|
591
605
|
headers,
|
package/lib/Utils/messages.js
CHANGED
|
@@ -133,7 +133,7 @@ export const prepareWAMessageMedia = async (message, options) => {
|
|
|
133
133
|
}
|
|
134
134
|
const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined';
|
|
135
135
|
const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined';
|
|
136
|
-
const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true;
|
|
136
|
+
const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true && typeof uploadData.waveform === 'undefined';
|
|
137
137
|
const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true;
|
|
138
138
|
const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation;
|
|
139
139
|
const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await encryptedStream(uploadData.media, options.mediaTypeOverride || mediaType, {
|
|
@@ -460,6 +460,12 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
460
460
|
}
|
|
461
461
|
}
|
|
462
462
|
}
|
|
463
|
+
else if (hasNonNullishProperty(message, 'album')) {
|
|
464
|
+
m.albumMessage = {
|
|
465
|
+
expectedImageCount: message.album.expectedImageCount,
|
|
466
|
+
expectedVideoCount: message.album.expectedVideoCount
|
|
467
|
+
};
|
|
468
|
+
}
|
|
463
469
|
else if (hasNonNullishProperty(message, 'sharePhoneNumber')) {
|
|
464
470
|
m.protocolMessage = {
|
|
465
471
|
type: proto.Message.ProtocolMessage.Type.SHARE_PHONE_NUMBER
|
|
@@ -485,15 +491,23 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
485
491
|
if (hasOptionalProperty(message, 'viewOnce') && !!message.viewOnce) {
|
|
486
492
|
m = { viewOnceMessage: { message: m } };
|
|
487
493
|
}
|
|
488
|
-
if (hasOptionalProperty(message, 'mentions') && message.mentions?.length)
|
|
494
|
+
if ((hasOptionalProperty(message, 'mentions') && message.mentions?.length) ||
|
|
495
|
+
(hasOptionalProperty(message, 'mentionAll') && message.mentionAll)) {
|
|
489
496
|
const messageType = Object.keys(m)[0];
|
|
490
497
|
const key = m[messageType];
|
|
491
|
-
if ('contextInfo' in key
|
|
492
|
-
key.contextInfo
|
|
498
|
+
if (key && 'contextInfo' in key) {
|
|
499
|
+
key.contextInfo = key.contextInfo || {};
|
|
500
|
+
if (message.mentions?.length) {
|
|
501
|
+
key.contextInfo.mentionedJid = message.mentions;
|
|
502
|
+
}
|
|
503
|
+
if (message.mentionAll) {
|
|
504
|
+
key.contextInfo.nonJidMentions = 1;
|
|
505
|
+
}
|
|
493
506
|
}
|
|
494
507
|
else if (key) {
|
|
495
508
|
key.contextInfo = {
|
|
496
|
-
mentionedJid: message.mentions
|
|
509
|
+
mentionedJid: message.mentions,
|
|
510
|
+
nonJidMentions: message.mentionAll ? 1 : 0
|
|
497
511
|
};
|
|
498
512
|
}
|
|
499
513
|
}
|
|
@@ -517,6 +531,15 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
517
531
|
key.contextInfo = message.contextInfo;
|
|
518
532
|
}
|
|
519
533
|
}
|
|
534
|
+
if (hasOptionalProperty(message, 'albumParentKey') && !!message.albumParentKey) {
|
|
535
|
+
m.messageContextInfo = {
|
|
536
|
+
...m.messageContextInfo,
|
|
537
|
+
messageAssociation: {
|
|
538
|
+
associationType: WAProto.MessageAssociation.AssociationType.MEDIA_ALBUM,
|
|
539
|
+
parentMessageKey: message.albumParentKey
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}
|
|
520
543
|
if (shouldIncludeReportingToken(m)) {
|
|
521
544
|
m.messageContextInfo = m.messageContextInfo || {};
|
|
522
545
|
if (!m.messageContextInfo.messageSecret) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a processor for offline stanza nodes that:
|
|
3
|
+
* - Queues nodes for sequential processing
|
|
4
|
+
* - Yields to the event loop periodically to avoid blocking
|
|
5
|
+
* - Catches handler errors to prevent the processing loop from crashing
|
|
6
|
+
*/
|
|
7
|
+
export function makeOfflineNodeProcessor(nodeProcessorMap, deps, batchSize = 10) {
|
|
8
|
+
const nodes = [];
|
|
9
|
+
let isProcessing = false;
|
|
10
|
+
const enqueue = (type, node) => {
|
|
11
|
+
nodes.push({ type, node });
|
|
12
|
+
if (isProcessing) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
isProcessing = true;
|
|
16
|
+
const promise = async () => {
|
|
17
|
+
let processedInBatch = 0;
|
|
18
|
+
while (nodes.length && deps.isWsOpen()) {
|
|
19
|
+
const { type, node } = nodes.shift();
|
|
20
|
+
const nodeProcessor = nodeProcessorMap.get(type);
|
|
21
|
+
if (!nodeProcessor) {
|
|
22
|
+
deps.onUnexpectedError(new Error(`unknown offline node type: ${type}`), 'processing offline node');
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
await nodeProcessor(node).catch(err => deps.onUnexpectedError(err, `processing offline ${type}`));
|
|
26
|
+
processedInBatch++;
|
|
27
|
+
// Yield to event loop after processing a batch
|
|
28
|
+
// This prevents blocking the event loop for too long when there are many offline nodes
|
|
29
|
+
if (processedInBatch >= batchSize) {
|
|
30
|
+
processedInBatch = 0;
|
|
31
|
+
await deps.yieldToEventLoop();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
isProcessing = false;
|
|
35
|
+
};
|
|
36
|
+
promise().catch(error => deps.onUnexpectedError(error, 'processing offline nodes'));
|
|
37
|
+
};
|
|
38
|
+
return { enqueue };
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=offline-node-processor.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Boom } from '@hapi/boom';
|
|
1
2
|
import { proto } from '../../WAProto/index.js';
|
|
2
3
|
import { WAMessageStubType } from '../Types/index.js';
|
|
3
4
|
import { getContentType, normalizeMessageContent } from '../Utils/messages.js';
|
|
@@ -5,6 +6,7 @@ import { areJidsSameUser, isHostedLidUser, isHostedPnUser, isJidBroadcast, isJid
|
|
|
5
6
|
import { aesDecryptGCM, hmacSign } from './crypto.js';
|
|
6
7
|
import { getKeyAuthor, toNumber } from './generics.js';
|
|
7
8
|
import { downloadAndProcessHistorySyncNotification } from './history.js';
|
|
9
|
+
import { buildMergedTcTokenIndexWrite, resolveTcTokenJid } from './tc-token-utils.js';
|
|
8
10
|
const REAL_MSG_STUB_TYPES = new Set([
|
|
9
11
|
WAMessageStubType.CALL_MISSED_GROUP_VIDEO,
|
|
10
12
|
WAMessageStubType.CALL_MISSED_GROUP_VOICE,
|
|
@@ -12,6 +14,53 @@ const REAL_MSG_STUB_TYPES = new Set([
|
|
|
12
14
|
WAMessageStubType.CALL_MISSED_VOICE
|
|
13
15
|
]);
|
|
14
16
|
const REAL_MSG_REQ_ME_STUB_TYPES = new Set([WAMessageStubType.GROUP_PARTICIPANT_ADD]);
|
|
17
|
+
async function storeTcTokensFromHistorySync(chats, signalRepository, keyStore, logger) {
|
|
18
|
+
const getLIDForPN = signalRepository.lidMapping.getLIDForPN.bind(signalRepository.lidMapping);
|
|
19
|
+
const candidates = [];
|
|
20
|
+
for (const chat of chats) {
|
|
21
|
+
const ts = chat.tcTokenTimestamp ? toNumber(chat.tcTokenTimestamp) : 0;
|
|
22
|
+
if (chat.tcToken?.length && ts > 0) {
|
|
23
|
+
const jid = jidNormalizedUser(chat.id);
|
|
24
|
+
const storageJid = await resolveTcTokenJid(jid, getLIDForPN);
|
|
25
|
+
candidates.push({
|
|
26
|
+
storageJid,
|
|
27
|
+
token: Buffer.from(chat.tcToken),
|
|
28
|
+
ts,
|
|
29
|
+
senderTs: chat.tcTokenSenderTimestamp ? toNumber(chat.tcTokenSenderTimestamp) : undefined
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (!candidates.length) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const jids = candidates.map(c => c.storageJid);
|
|
37
|
+
const existing = await keyStore.get('tctoken', jids);
|
|
38
|
+
const entries = {};
|
|
39
|
+
for (const c of candidates) {
|
|
40
|
+
const existingEntry = existing[c.storageJid];
|
|
41
|
+
const existingTs = existingEntry?.timestamp ? Number(existingEntry.timestamp) : 0;
|
|
42
|
+
if (existingTs > 0 && existingTs >= c.ts) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
entries[c.storageJid] = {
|
|
46
|
+
...existingEntry,
|
|
47
|
+
token: c.token,
|
|
48
|
+
timestamp: String(c.ts),
|
|
49
|
+
...(c.senderTs !== undefined ? { senderTimestamp: c.senderTs } : {})
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (Object.keys(entries).length) {
|
|
53
|
+
logger?.debug({ count: Object.keys(entries).length }, 'storing tctokens from history sync');
|
|
54
|
+
try {
|
|
55
|
+
// Include updated __index so cross-session pruning picks these JIDs up.
|
|
56
|
+
const indexWrite = await buildMergedTcTokenIndexWrite(keyStore, Object.keys(entries));
|
|
57
|
+
await keyStore.set({ tctoken: { ...entries, ...indexWrite } });
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
logger?.warn({ err }, 'failed to store tctokens from history sync');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
15
64
|
/** Cleans a received message to further processing */
|
|
16
65
|
export const cleanMessage = (message, meId, meLid) => {
|
|
17
66
|
// ensure remoteJid and participant doesn't have device or agent in it
|
|
@@ -73,7 +122,17 @@ export const shouldIncrementChatUnread = (message) => !message.key.fromMe && !me
|
|
|
73
122
|
* Typically -- that'll be the remoteJid, but for broadcasts, it'll be the participant
|
|
74
123
|
*/
|
|
75
124
|
export const getChatId = ({ remoteJid, participant, fromMe }) => {
|
|
125
|
+
if (!remoteJid) {
|
|
126
|
+
throw new Boom('Cannot derive chat id: message key is missing remoteJid', {
|
|
127
|
+
data: { remoteJid, participant, fromMe }
|
|
128
|
+
});
|
|
129
|
+
}
|
|
76
130
|
if (isJidBroadcast(remoteJid) && !isJidStatusBroadcast(remoteJid) && !fromMe) {
|
|
131
|
+
if (!participant) {
|
|
132
|
+
throw new Boom('Cannot derive chat id: broadcast message key is missing participant', {
|
|
133
|
+
data: { remoteJid, fromMe }
|
|
134
|
+
});
|
|
135
|
+
}
|
|
77
136
|
return participant;
|
|
78
137
|
}
|
|
79
138
|
return remoteJid;
|
|
@@ -146,6 +205,39 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
|
|
|
146
205
|
}
|
|
147
206
|
const protocolMsg = content?.protocolMessage;
|
|
148
207
|
if (protocolMsg) {
|
|
208
|
+
// Mirror whatsmeow's `handleProtocolMessage` guard, but applied only to
|
|
209
|
+
// the protocol message types that originate from our own device — an
|
|
210
|
+
// attacker could otherwise spoof any of these to manipulate local state.
|
|
211
|
+
//
|
|
212
|
+
// Self-only types (drop if `!fromMe`):
|
|
213
|
+
// - HISTORY_SYNC_NOTIFICATION (our phone driving history sync)
|
|
214
|
+
// - APP_STATE_SYNC_KEY_SHARE (key share between our devices)
|
|
215
|
+
// - LID_MIGRATION_MAPPING_SYNC (server-initiated via our phone)
|
|
216
|
+
// - PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE (response from our phone to our PDO request)
|
|
217
|
+
//
|
|
218
|
+
// Cross-user types (must NOT be dropped — legitimately arrive from others):
|
|
219
|
+
// - REVOKE
|
|
220
|
+
// - MESSAGE_EDIT
|
|
221
|
+
// - EPHEMERAL_SETTING
|
|
222
|
+
// - GROUP_MEMBER_LABEL_CHANGE
|
|
223
|
+
//
|
|
224
|
+
// See https://github.com/tulir/whatsmeow/blob/8d3700152a/message.go#L842-L845
|
|
225
|
+
// for the reference architecture — whatsmeow's `handleProtocolMessage`
|
|
226
|
+
// only contains self-only types because edits are unwrapped from
|
|
227
|
+
// `EditedMessage` BEFORE this dispatch and revokes aren't routed here.
|
|
228
|
+
const SELF_ONLY_TYPES = new Set([
|
|
229
|
+
proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION,
|
|
230
|
+
proto.Message.ProtocolMessage.Type.APP_STATE_SYNC_KEY_SHARE,
|
|
231
|
+
proto.Message.ProtocolMessage.Type.LID_MIGRATION_MAPPING_SYNC,
|
|
232
|
+
proto.Message.ProtocolMessage.Type.PEER_DATA_OPERATION_REQUEST_RESPONSE_MESSAGE
|
|
233
|
+
]);
|
|
234
|
+
if (protocolMsg.type !== null &&
|
|
235
|
+
protocolMsg.type !== undefined &&
|
|
236
|
+
SELF_ONLY_TYPES.has(protocolMsg.type) &&
|
|
237
|
+
!message.key.fromMe) {
|
|
238
|
+
logger?.warn({ msgId: message.key.id, type: protocolMsg.type, from: message.key.participant || message.key.remoteJid }, 'dropping spoofed self-only protocolMessage from non-self origin');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
149
241
|
switch (protocolMsg.type) {
|
|
150
242
|
case proto.Message.ProtocolMessage.Type.HISTORY_SYNC_NOTIFICATION:
|
|
151
243
|
const histNotification = protocolMsg.historySyncNotification;
|
|
@@ -174,9 +266,11 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
|
|
|
174
266
|
.storeLIDPNMappings(data.lidPnMappings)
|
|
175
267
|
.catch(err => logger?.warn({ err }, 'failed to store LID-PN mappings from history sync'));
|
|
176
268
|
}
|
|
269
|
+
await storeTcTokensFromHistorySync(data.chats, signalRepository, keyStore, logger);
|
|
177
270
|
ev.emit('messaging-history.set', {
|
|
178
271
|
...data,
|
|
179
272
|
isLatest: histNotification.syncType !== proto.HistorySync.HistorySyncType.ON_DEMAND ? isLatest : undefined,
|
|
273
|
+
chunkOrder: histNotification.chunkOrder,
|
|
180
274
|
peerDataRequestSessionId: histNotification.peerDataRequestSessionId
|
|
181
275
|
});
|
|
182
276
|
}
|
|
@@ -383,12 +477,19 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
|
|
|
383
477
|
id: jid,
|
|
384
478
|
author: message.key.participant,
|
|
385
479
|
authorPn: message.key.participantAlt,
|
|
480
|
+
authorUsername: message.key.participantUsername,
|
|
386
481
|
participants,
|
|
387
482
|
action
|
|
388
483
|
});
|
|
389
484
|
const emitGroupUpdate = (update) => {
|
|
390
485
|
ev.emit('groups.update', [
|
|
391
|
-
{
|
|
486
|
+
{
|
|
487
|
+
id: jid,
|
|
488
|
+
...update,
|
|
489
|
+
author: message.key.participant ?? undefined,
|
|
490
|
+
authorPn: message.key.participantAlt,
|
|
491
|
+
authorUsername: message.key.participantUsername
|
|
492
|
+
}
|
|
392
493
|
]);
|
|
393
494
|
};
|
|
394
495
|
const emitGroupRequestJoin = (participant, action, method) => {
|
|
@@ -396,6 +497,7 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
|
|
|
396
497
|
id: jid,
|
|
397
498
|
author: message.key.participant,
|
|
398
499
|
authorPn: message.key.participantAlt,
|
|
500
|
+
authorUsername: message.key.participantUsername,
|
|
399
501
|
participant: participant.lid,
|
|
400
502
|
participantPn: participant.pn,
|
|
401
503
|
action,
|
package/lib/Utils/signal.js
CHANGED
|
@@ -55,6 +55,48 @@ export const xmppPreKey = (pair, id) => ({
|
|
|
55
55
|
{ tag: 'value', attrs: {}, content: pair.public }
|
|
56
56
|
]
|
|
57
57
|
});
|
|
58
|
+
const isValidUInt = (n) => typeof n === 'number' && Number.isInteger(n);
|
|
59
|
+
export const extractE2ESessionFromRetryReceipt = (receipt) => {
|
|
60
|
+
const keysNode = getBinaryNodeChild(receipt, 'keys');
|
|
61
|
+
if (!keysNode)
|
|
62
|
+
return null;
|
|
63
|
+
const typeBuf = getBinaryNodeChildBuffer(keysNode, 'type');
|
|
64
|
+
if (!typeBuf || typeBuf.length !== 1 || typeBuf[0] !== KEY_BUNDLE_TYPE[0])
|
|
65
|
+
return null;
|
|
66
|
+
const identity = getBinaryNodeChildBuffer(keysNode, 'identity');
|
|
67
|
+
const skey = getBinaryNodeChild(keysNode, 'skey');
|
|
68
|
+
if (!identity || identity.length !== 32 || !skey)
|
|
69
|
+
return null;
|
|
70
|
+
const registrationId = getBinaryNodeChildUInt(receipt, 'registration', 4);
|
|
71
|
+
if (!isValidUInt(registrationId))
|
|
72
|
+
return null;
|
|
73
|
+
const signedPubKey = getBinaryNodeChildBuffer(skey, 'value');
|
|
74
|
+
const signedSig = getBinaryNodeChildBuffer(skey, 'signature');
|
|
75
|
+
const signedKeyId = getBinaryNodeChildUInt(skey, 'id', 3);
|
|
76
|
+
if (!signedPubKey || signedPubKey.length !== 32 || !signedSig || !isValidUInt(signedKeyId)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const preKeyNode = getBinaryNodeChild(keysNode, 'key');
|
|
80
|
+
let preKey;
|
|
81
|
+
if (preKeyNode) {
|
|
82
|
+
const preKeyPub = getBinaryNodeChildBuffer(preKeyNode, 'value');
|
|
83
|
+
const preKeyId = getBinaryNodeChildUInt(preKeyNode, 'id', 3);
|
|
84
|
+
if (!preKeyPub || preKeyPub.length !== 32 || !isValidUInt(preKeyId)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
preKey = { keyId: preKeyId, publicKey: generateSignalPubKey(preKeyPub) };
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
registrationId,
|
|
91
|
+
identityKey: generateSignalPubKey(identity),
|
|
92
|
+
signedPreKey: {
|
|
93
|
+
keyId: signedKeyId,
|
|
94
|
+
publicKey: generateSignalPubKey(signedPubKey),
|
|
95
|
+
signature: signedSig
|
|
96
|
+
},
|
|
97
|
+
preKey
|
|
98
|
+
};
|
|
99
|
+
};
|
|
58
100
|
export const parseAndInjectE2ESessions = async (node, repository) => {
|
|
59
101
|
const extractKey = (key) => key
|
|
60
102
|
? {
|