@violetix/baileys 0.5.6 → 0.6.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 +1530 -2009
- package/WAProto/index.d.ts +14030 -14586
- package/WAProto/index.js +58426 -88874
- package/lib/Defaults/index.d.ts +2 -7
- package/lib/Defaults/index.js +5 -20
- package/lib/Signal/Group/sender-key-message.js +19 -1
- package/lib/Signal/libsignal.js +82 -29
- package/lib/Signal/xeddsa-fast.js +114 -0
- package/lib/Socket/Client/types.js +6 -8
- package/lib/Socket/Client/websocket.js +3 -38
- package/lib/Socket/business.d.ts +0 -3
- package/lib/Socket/chats.d.ts +0 -3
- package/lib/Socket/chats.js +18 -45
- package/lib/Socket/communities.d.ts +0 -3
- package/lib/Socket/graphql.js +3 -7
- package/lib/Socket/groups.d.ts +0 -3
- package/lib/Socket/groups.js +3 -8
- package/lib/Socket/index.d.ts +15 -17
- package/lib/Socket/messages-recv.d.ts +0 -3
- package/lib/Socket/messages-recv.js +43 -127
- package/lib/Socket/messages-send.d.ts +0 -4
- package/lib/Socket/messages-send.js +63 -191
- package/lib/Socket/newsletter.d.ts +0 -3
- package/lib/Socket/newsletter.js +1 -2
- package/lib/Socket/newsletter.js.map +1 -1
- package/lib/Socket/socket.d.ts +0 -9
- package/lib/Socket/socket.js +53 -346
- package/lib/Store/make-in-memory-store.js +30 -41
- package/lib/Types/Auth.d.ts +2 -60
- package/lib/Types/Bussines.d.ts +2 -9
- package/lib/Types/Call.d.ts +2 -22
- package/lib/Types/Chat.d.ts +1 -2
- package/lib/Types/Contact.d.ts +2 -11
- package/lib/Types/Events.d.ts +2 -41
- package/lib/Types/GroupMetadata.d.ts +2 -36
- package/lib/Types/Message.d.ts +1 -11
- package/lib/Types/Product.d.ts +2 -5
- package/lib/Types/Signal.d.ts +2 -10
- package/lib/Types/Socket.d.ts +2 -33
- package/lib/Types/USync.d.ts +2 -9
- package/lib/Types/index.d.ts +1 -3
- package/lib/Utils/auth-utils.d.ts +2 -3
- package/lib/Utils/auth-utils.js +53 -37
- package/lib/Utils/business.js +11 -25
- package/lib/Utils/chat-utils.js +4 -6
- package/lib/Utils/decode-wa-message.js +2 -2
- package/lib/Utils/event-buffer.js +12 -21
- package/lib/Utils/generics.d.ts +5 -5
- package/lib/Utils/generics.js +7 -25
- package/lib/Utils/history.d.ts +1 -2
- package/lib/Utils/history.js +83 -137
- package/lib/Utils/index.d.ts +0 -4
- package/lib/Utils/index.js +0 -4
- package/lib/Utils/listener-guard.js +0 -2
- package/lib/Utils/message-retry-manager.js +0 -20
- package/lib/Utils/messages-media.d.ts +8 -39
- package/lib/Utils/messages-media.js +89 -218
- package/lib/Utils/messages.d.ts +0 -1
- package/lib/Utils/messages.js +36 -122
- package/lib/Utils/process-message.d.ts +0 -18
- package/lib/Utils/process-message.js +47 -99
- package/lib/Utils/rich-message-utils.js +4 -3
- package/lib/Utils/use-multi-file-auth-state.d.ts +3 -5
- package/lib/Utils/use-multi-file-auth-state.js +6 -27
- package/lib/Utils/use-single-file-auth-state.d.ts +1 -1
- package/lib/Utils/use-single-file-auth-state.js +3 -12
- package/lib/WABinary/types.d.ts +2 -6
- package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +3 -6
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +2 -3
- package/lib/WAUSync/USyncQuery.d.ts +1 -1
- package/lib/WAUSync/USyncQuery.js +4 -7
- package/package.json +107 -123
- package/lib/Utils/a2ui.d.ts +0 -50
- package/lib/Utils/a2ui.js +0 -168
- package/lib/Utils/anchor-guard.d.ts +0 -71
- package/lib/Utils/anchor-guard.js +0 -404
- package/lib/Utils/connection-manager.d.ts +0 -44
- package/lib/Utils/connection-manager.js +0 -258
- package/lib/Utils/ignore-key.d.ts +0 -51
- package/lib/Utils/ignore-key.js +0 -176
package/lib/Defaults/index.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export const BIZ_BOT_SUPPORT_PAYLOAD: "{\"version\":1,\"is_ai_message\":true,\"s
|
|
|
3
3
|
export const DEFAULT_ORIGIN: "https://web.whatsapp.com";
|
|
4
4
|
export const CALL_VIDEO_PREFIX: "https://call.whatsapp.com/video/";
|
|
5
5
|
export const CALL_AUDIO_PREFIX: "https://call.whatsapp.com/voice/";
|
|
6
|
-
export const DONATE_URL: "https://
|
|
7
|
-
export const LIBRARY_NAME: "
|
|
6
|
+
export const DONATE_URL: "https://saweria.co/itsliaaa";
|
|
7
|
+
export const LIBRARY_NAME: "itsliaaa/baileys";
|
|
8
8
|
export const DEF_CALLBACK_PREFIX: "CB:";
|
|
9
9
|
export const DEF_TAG_PREFIX: "TAG:";
|
|
10
10
|
export const PHONE_CONNECTION_CB: "CB:Pong";
|
|
@@ -41,11 +41,6 @@ export namespace DEFAULT_CONNECTION_CONFIG {
|
|
|
41
41
|
export let browser: any[];
|
|
42
42
|
export let waWebSocketUrl: string;
|
|
43
43
|
export let connectTimeoutMs: number;
|
|
44
|
-
export let closeTimeoutMs: number;
|
|
45
|
-
export let autoReconnect: boolean;
|
|
46
|
-
export let reconnectDelayMs: number;
|
|
47
|
-
export let maxReconnectDelayMs: number;
|
|
48
|
-
export let maxReconnectAttempts: number;
|
|
49
44
|
export let keepAliveIntervalMs: number;
|
|
50
45
|
export let logger: any;
|
|
51
46
|
export let emitOwnEvents: boolean;
|
package/lib/Defaults/index.js
CHANGED
|
@@ -2,17 +2,14 @@ import { proto } from '../../WAProto/index.js';
|
|
|
2
2
|
import { makeLibSignalRepository } from '../Signal/libsignal.js';
|
|
3
3
|
import { Browsers } from '../Utils/browser-utils.js';
|
|
4
4
|
import logger from '../Utils/logger.js';
|
|
5
|
-
const version = [2, 3000,
|
|
5
|
+
const version = [2, 3000, 1045821369];
|
|
6
6
|
export const UNAUTHORIZED_CODES = [401, 403, 419];
|
|
7
7
|
export const BIZ_BOT_SUPPORT_PAYLOAD = '{"version":1,"is_ai_message":true,"should_upload_client_logs":false,"should_show_system_message":false,"ticket_id":"7004947587700716","citation_items":[],"ticket_locale":"us"}';
|
|
8
8
|
export const DEFAULT_ORIGIN = 'https://web.whatsapp.com';
|
|
9
9
|
export const CALL_VIDEO_PREFIX = 'https://call.whatsapp.com/video/';
|
|
10
10
|
export const CALL_AUDIO_PREFIX = 'https://call.whatsapp.com/voice/';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// meant every bot on this fork advertised it in real messages; it points at the repository now.
|
|
14
|
-
export const DONATE_URL = 'https://github.com/cv3inx/bv2';
|
|
15
|
-
export const LIBRARY_NAME = 'violetix/baileys';
|
|
11
|
+
export const DONATE_URL = 'https://saweria.co/itsliaaa'; // Lia@Note 02-02-26 --- Apologies if this feels cheeky, just a fallback
|
|
12
|
+
export const LIBRARY_NAME = 'itsliaaa/baileys';
|
|
16
13
|
export const DEF_CALLBACK_PREFIX = 'CB:';
|
|
17
14
|
export const DEF_TAG_PREFIX = 'TAG:';
|
|
18
15
|
export const PHONE_CONNECTION_CB = 'CB:Pong';
|
|
@@ -57,14 +54,6 @@ export const DEFAULT_CONNECTION_CONFIG = {
|
|
|
57
54
|
browser: Browsers.macOS('Chrome'),
|
|
58
55
|
waWebSocketUrl: 'wss://web.whatsapp.com/ws/chat',
|
|
59
56
|
connectTimeoutMs: 20000,
|
|
60
|
-
// a half-open socket never answers close(); without a ceiling every reconnect leaks one
|
|
61
|
-
closeTimeoutMs: 5000,
|
|
62
|
-
// reconnect the transport in place instead of ending the socket, so a host that treats
|
|
63
|
-
// connection 'close' as "build a new socket" never runs two sockets for one account
|
|
64
|
-
autoReconnect: true,
|
|
65
|
-
reconnectDelayMs: 2000,
|
|
66
|
-
maxReconnectDelayMs: 60000,
|
|
67
|
-
maxReconnectAttempts: 10,
|
|
68
57
|
keepAliveIntervalMs: 30000,
|
|
69
58
|
keepAliveGracePeriodMs: 30000,
|
|
70
59
|
logger: logger.child({ class: 'baileys' }),
|
|
@@ -76,13 +65,9 @@ export const DEFAULT_CONNECTION_CONFIG = {
|
|
|
76
65
|
fireInitQueries: true,
|
|
77
66
|
auth: undefined,
|
|
78
67
|
markOnlineOnConnect: true,
|
|
79
|
-
|
|
80
|
-
// reconnect load (upstream default is false too)
|
|
81
|
-
syncFullHistory: false,
|
|
68
|
+
syncFullHistory: false, // OPTIMIZED: FORCE DISABLED
|
|
82
69
|
patchMessageBeforeSending: msg => msg,
|
|
83
|
-
shouldSyncHistoryMessage: (
|
|
84
|
-
return syncType !== proto.HistorySync.HistorySyncType.FULL;
|
|
85
|
-
},
|
|
70
|
+
shouldSyncHistoryMessage: () => false, // OPTIMIZED: ALWAYS FALSE
|
|
86
71
|
shouldIgnoreJid: () => false,
|
|
87
72
|
linkPreviewImageThumbnailWidth: 192,
|
|
88
73
|
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 },
|
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
import { calculateSignature, verifySignature } from 'libsignal/src/curve.js';
|
|
1
|
+
import { calculateSignature as libCalculateSignature, verifySignature as libVerifySignature } from 'libsignal/src/curve.js';
|
|
2
|
+
import { calculateSignature as fastCalculateSignature, verifySignature as fastVerifySignature } from '../xeddsa-fast.js';
|
|
3
|
+
|
|
4
|
+
const calculateSignature = (privateKey, message) => {
|
|
5
|
+
try {
|
|
6
|
+
return fastCalculateSignature(privateKey, message);
|
|
7
|
+
}
|
|
8
|
+
catch (_) {
|
|
9
|
+
return libCalculateSignature(privateKey, message);
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const verifySignature = (publicKey, message, signature) => {
|
|
13
|
+
try {
|
|
14
|
+
return fastVerifySignature(publicKey, message, signature);
|
|
15
|
+
}
|
|
16
|
+
catch (_) {
|
|
17
|
+
return libVerifySignature(publicKey, message, signature);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
2
20
|
import { proto } from '../../../WAProto/index.js';
|
|
3
21
|
import { CiphertextMessage } from './ciphertext-message.js';
|
|
4
22
|
export class SenderKeyMessage extends CiphertextMessage {
|
package/lib/Signal/libsignal.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import * as libsignal from 'libsignal';
|
|
3
3
|
// @ts-ignore
|
|
4
4
|
import { PreKeyWhisperMessage } from 'libsignal/src/protobufs.js';
|
|
5
|
+
import { LRUCache } from 'lru-cache';
|
|
5
6
|
import { generateSignalPubKey } from '../Utils/index.js';
|
|
6
7
|
import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, transferDevice, WAJIDDomains } from '../WABinary/index.js';
|
|
7
8
|
import { SenderKeyName } from './Group/sender-key-name.js';
|
|
@@ -32,10 +33,26 @@ function extractIdentityFromPkmsg(ciphertext) {
|
|
|
32
33
|
}
|
|
33
34
|
export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
34
35
|
const lidMapping = new LIDMappingStore(auth.keys, logger, pnToLIDFunc);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const sessionRecordCache = new LRUCache({
|
|
37
|
+
max: 5000,
|
|
38
|
+
ttl: 30 * 60 * 1000,
|
|
39
|
+
ttlAutopurge: true,
|
|
40
|
+
updateAgeOnGet: true
|
|
41
|
+
});
|
|
42
|
+
const senderKeyCache = new LRUCache({
|
|
43
|
+
max: 5000,
|
|
44
|
+
ttl: 30 * 60 * 1000,
|
|
45
|
+
ttlAutopurge: true,
|
|
46
|
+
updateAgeOnGet: true
|
|
47
|
+
});
|
|
48
|
+
const storage = signalStorage(auth, lidMapping, sessionRecordCache, senderKeyCache);
|
|
38
49
|
const parsedKeys = auth.keys;
|
|
50
|
+
const migratedSessionCache = new LRUCache({
|
|
51
|
+
ttl: 3 * 24 * 60 * 60 * 1000, // 3 days
|
|
52
|
+
ttlAutopurge: true,
|
|
53
|
+
updateAgeOnGet: true,
|
|
54
|
+
max: 5000
|
|
55
|
+
});
|
|
39
56
|
const ensureSenderKeyAndCreateSkdm = async (group, meId) => {
|
|
40
57
|
const senderName = jidToSignalSenderKeyName(group, meId);
|
|
41
58
|
const senderNameStr = senderName.toString();
|
|
@@ -62,8 +79,12 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
62
79
|
}
|
|
63
80
|
const senderName = jidToSignalSenderKeyName(item.groupId, authorJid);
|
|
64
81
|
const senderMsg = new SenderKeyDistributionMessage(null, null, null, null, item.axolotlSenderKeyDistributionMessage);
|
|
82
|
+
const senderNameStr = senderName.toString();
|
|
83
|
+
const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr]);
|
|
84
|
+
if (!senderKey) {
|
|
85
|
+
await storage.storeSenderKey(senderName, new SenderKeyRecord());
|
|
86
|
+
}
|
|
65
87
|
return parsedKeys.transaction(async () => {
|
|
66
|
-
const senderNameStr = senderName.toString();
|
|
67
88
|
const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr]);
|
|
68
89
|
if (!senderKey) {
|
|
69
90
|
await storage.storeSenderKey(senderName, new SenderKeyRecord());
|
|
@@ -74,6 +95,17 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
74
95
|
async decryptMessage({ jid, type, ciphertext }) {
|
|
75
96
|
const addr = jidToSignalProtocolAddress(jid);
|
|
76
97
|
const session = new libsignal.SessionCipher(storage, addr);
|
|
98
|
+
// Extract and save sender's identity key before decryption for identity change detection
|
|
99
|
+
if (type === 'pkmsg') {
|
|
100
|
+
const identityKey = extractIdentityFromPkmsg(ciphertext);
|
|
101
|
+
if (identityKey) {
|
|
102
|
+
const addrStr = addr.toString();
|
|
103
|
+
const identityChanged = await storage.saveIdentity(addrStr, identityKey);
|
|
104
|
+
if (identityChanged) {
|
|
105
|
+
logger.info({ jid, addr: addrStr }, 'identity key changed or new contact, session will be re-established');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
77
109
|
async function doDecrypt() {
|
|
78
110
|
let result;
|
|
79
111
|
switch (type) {
|
|
@@ -89,15 +121,6 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
89
121
|
// If it's not a sync message, we need to ensure atomicity
|
|
90
122
|
// For regular messages, we use a transaction to ensure atomicity
|
|
91
123
|
return parsedKeys.transaction(async () => {
|
|
92
|
-
if (type === 'pkmsg') {
|
|
93
|
-
const identityKey = extractIdentityFromPkmsg(ciphertext);
|
|
94
|
-
if (identityKey) {
|
|
95
|
-
const identityChanged = await storage.saveIdentity(addr.toString(), identityKey);
|
|
96
|
-
if (identityChanged) {
|
|
97
|
-
logger.info({ jid, addr: addr.toString() }, 'identity key changed or new contact, session will be re-established');
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
124
|
return await doDecrypt();
|
|
102
125
|
}, jid);
|
|
103
126
|
},
|
|
@@ -176,18 +199,21 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
176
199
|
async deleteSession(jids) {
|
|
177
200
|
if (!jids.length)
|
|
178
201
|
return;
|
|
202
|
+
// Convert JIDs to signal addresses and prepare for bulk deletion
|
|
203
|
+
const sessionUpdates = {};
|
|
204
|
+
jids.forEach(jid => {
|
|
205
|
+
const addr = jidToSignalProtocolAddress(jid);
|
|
206
|
+
sessionUpdates[addr.toString()] = null;
|
|
207
|
+
});
|
|
179
208
|
// Single transaction for all deletions
|
|
180
209
|
return parsedKeys.transaction(async () => {
|
|
181
|
-
const sessionUpdates = {};
|
|
182
|
-
for (const jid of jids) {
|
|
183
|
-
const addr = jidToSignalProtocolAddress(jid).toString();
|
|
184
|
-
sessionUpdates[addr] = null;
|
|
185
|
-
sessionUpdates[await storage.resolveLIDSignalAddress(addr)] = null;
|
|
186
|
-
}
|
|
187
210
|
await auth.keys.set({ session: sessionUpdates });
|
|
188
211
|
}, `delete-${jids.length}-sessions`);
|
|
189
212
|
},
|
|
190
213
|
close() {
|
|
214
|
+
migratedSessionCache.clear();
|
|
215
|
+
sessionRecordCache.clear();
|
|
216
|
+
senderKeyCache.clear();
|
|
191
217
|
lidMapping.close();
|
|
192
218
|
},
|
|
193
219
|
async migrateSession(fromJid, toJid) {
|
|
@@ -201,18 +227,22 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
201
227
|
const { user } = jidDecode(fromJid);
|
|
202
228
|
logger.debug({ fromJid }, 'bulk device migration - loading all user devices');
|
|
203
229
|
// Get user's device list from storage
|
|
204
|
-
const { [user]:
|
|
205
|
-
if (!
|
|
230
|
+
const { [user]: userDevices } = await parsedKeys.get('device-list', [user]);
|
|
231
|
+
if (!userDevices) {
|
|
206
232
|
return { migrated: 0, skipped: 0, total: 0 };
|
|
207
233
|
}
|
|
208
|
-
const userDevices = [...storedDevices];
|
|
209
234
|
const { device: fromDevice } = jidDecode(fromJid);
|
|
210
235
|
const fromDeviceStr = fromDevice?.toString() || '0';
|
|
211
236
|
if (!userDevices.includes(fromDeviceStr)) {
|
|
212
237
|
userDevices.push(fromDeviceStr);
|
|
213
238
|
}
|
|
214
|
-
//
|
|
215
|
-
const
|
|
239
|
+
// Filter out cached devices before database fetch
|
|
240
|
+
const uncachedDevices = userDevices.filter(device => {
|
|
241
|
+
const deviceKey = `${user}.${device}`;
|
|
242
|
+
return !migratedSessionCache.has(deviceKey);
|
|
243
|
+
});
|
|
244
|
+
// Bulk check session existence only for uncached devices
|
|
245
|
+
const deviceSessionKeys = uncachedDevices.map(device => `${user}.${device}`);
|
|
216
246
|
const existingSessions = await parsedKeys.get('session', deviceSessionKeys);
|
|
217
247
|
// Step 3: Convert existing sessions to JIDs (only migrate sessions that exist)
|
|
218
248
|
const deviceJids = [];
|
|
@@ -240,7 +270,14 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
240
270
|
return parsedKeys.transaction(async () => {
|
|
241
271
|
const migrationOps = deviceJids.map(jid => {
|
|
242
272
|
const lidWithDevice = transferDevice(jid, toJid);
|
|
273
|
+
const fromDecoded = jidDecode(jid);
|
|
274
|
+
const toDecoded = jidDecode(lidWithDevice);
|
|
243
275
|
return {
|
|
276
|
+
fromJid: jid,
|
|
277
|
+
toJid: lidWithDevice,
|
|
278
|
+
pnUser: fromDecoded.user,
|
|
279
|
+
lidUser: toDecoded.user,
|
|
280
|
+
deviceId: fromDecoded.device || 0,
|
|
244
281
|
fromAddr: jidToSignalProtocolAddress(jid),
|
|
245
282
|
toAddr: jidToSignalProtocolAddress(lidWithDevice)
|
|
246
283
|
};
|
|
@@ -271,6 +308,13 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
271
308
|
if (Object.keys(sessionUpdates).length > 0) {
|
|
272
309
|
await parsedKeys.set({ session: sessionUpdates });
|
|
273
310
|
logger.debug({ migratedSessions: migratedCount }, 'bulk session migration complete');
|
|
311
|
+
// Cache device-level migrations
|
|
312
|
+
for (const op of migrationOps) {
|
|
313
|
+
if (sessionUpdates[op.toAddr.toString()]) {
|
|
314
|
+
const deviceKey = `${op.pnUser}.${op.deviceId}`;
|
|
315
|
+
migratedSessionCache.set(deviceKey, true);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
274
318
|
}
|
|
275
319
|
const skippedCount = totalOps - migratedCount;
|
|
276
320
|
return { migrated: migratedCount, skipped: skippedCount, total: totalOps };
|
|
@@ -295,7 +339,7 @@ const jidToSignalProtocolAddress = (jid) => {
|
|
|
295
339
|
const jidToSignalSenderKeyName = (group, user) => {
|
|
296
340
|
return new SenderKeyName(group, jidToSignalProtocolAddress(user));
|
|
297
341
|
};
|
|
298
|
-
function signalStorage({ creds, keys }, lidMapping) {
|
|
342
|
+
function signalStorage({ creds, keys }, lidMapping, sessionRecordCache, senderKeyCache) {
|
|
299
343
|
// Shared function to resolve PN signal address to LID if mapping exists
|
|
300
344
|
const resolveLIDSignalAddress = async (id) => {
|
|
301
345
|
if (id.includes('.')) {
|
|
@@ -314,13 +358,16 @@ function signalStorage({ creds, keys }, lidMapping) {
|
|
|
314
358
|
return id;
|
|
315
359
|
};
|
|
316
360
|
return {
|
|
317
|
-
resolveLIDSignalAddress,
|
|
318
361
|
loadSession: async (id) => {
|
|
319
362
|
try {
|
|
320
363
|
const wireJid = await resolveLIDSignalAddress(id);
|
|
364
|
+
const cached = sessionRecordCache.get(wireJid);
|
|
365
|
+
if (cached) return cached;
|
|
321
366
|
const { [wireJid]: sess } = await keys.get('session', [wireJid]);
|
|
322
367
|
if (sess) {
|
|
323
|
-
|
|
368
|
+
const record = libsignal.SessionRecord.deserialize(sess);
|
|
369
|
+
sessionRecordCache.set(wireJid, record);
|
|
370
|
+
return record;
|
|
324
371
|
}
|
|
325
372
|
}
|
|
326
373
|
catch (e) {
|
|
@@ -330,6 +377,7 @@ function signalStorage({ creds, keys }, lidMapping) {
|
|
|
330
377
|
},
|
|
331
378
|
storeSession: async (id, session) => {
|
|
332
379
|
const wireJid = await resolveLIDSignalAddress(id);
|
|
380
|
+
sessionRecordCache.set(wireJid, session);
|
|
333
381
|
await keys.set({ session: { [wireJid]: session.serialize() } });
|
|
334
382
|
},
|
|
335
383
|
isTrustedIdentity: () => {
|
|
@@ -379,14 +427,19 @@ function signalStorage({ creds, keys }, lidMapping) {
|
|
|
379
427
|
},
|
|
380
428
|
loadSenderKey: async (senderKeyName) => {
|
|
381
429
|
const keyId = senderKeyName.toString();
|
|
430
|
+
const cached = senderKeyCache.get(keyId);
|
|
431
|
+
if (cached) return cached;
|
|
382
432
|
const { [keyId]: key } = await keys.get('sender-key', [keyId]);
|
|
383
433
|
if (key) {
|
|
384
|
-
|
|
434
|
+
const record = SenderKeyRecord.deserialize(key);
|
|
435
|
+
senderKeyCache.set(keyId, record);
|
|
436
|
+
return record;
|
|
385
437
|
}
|
|
386
438
|
return new SenderKeyRecord();
|
|
387
439
|
},
|
|
388
440
|
storeSenderKey: async (senderKeyName, key) => {
|
|
389
441
|
const keyId = senderKeyName.toString();
|
|
442
|
+
senderKeyCache.set(keyId, key);
|
|
390
443
|
const serialized = JSON.stringify(key.serialize());
|
|
391
444
|
await keys.set({ 'sender-key': { [keyId]: Buffer.from(serialized, 'utf-8') } });
|
|
392
445
|
},
|
|
@@ -400,4 +453,4 @@ function signalStorage({ creds, keys }, lidMapping) {
|
|
|
400
453
|
}
|
|
401
454
|
};
|
|
402
455
|
}
|
|
403
|
-
//# sourceMappingURL=libsignal.js.map
|
|
456
|
+
//# sourceMappingURL=libsignal.js.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Akselerator XEdDSA untuk grup message (SenderKey) encryption.
|
|
2
|
+
//
|
|
3
|
+
// curve25519-js (via libsignal) mengimplementasikan sign/verify dalam
|
|
4
|
+
// JavaScript murni: ~18-19 ms per operasi. Pada host dengan 100+ session,
|
|
5
|
+
// setiap pesan grup memanggil verifySignature (decrypt) dan calculateSignature
|
|
6
|
+
// (encrypt), sehingga operasi ini mendominasi event loop dan memicu CPU spike
|
|
7
|
+
// >250%.
|
|
8
|
+
//
|
|
9
|
+
// Restart-free & aman untuk session: hasil signature byte-per-byte identik
|
|
10
|
+
// dengan curve25519-js (diverifikasi 100/100), jadi WhatsApp menerima seperti
|
|
11
|
+
// biasa dan session lama tetap valid — tidak ada rekey / reconnect.
|
|
12
|
+
//
|
|
13
|
+
// Bila @noble/curves tidak tersedia, pemanggil jatuh kembali ke libsignal
|
|
14
|
+
// sehingga tidak pernah lebih buruk dari perilaku asli.
|
|
15
|
+
import { createHash } from 'crypto';
|
|
16
|
+
import { ed25519 } from '@noble/curves/ed25519.js';
|
|
17
|
+
|
|
18
|
+
const L = 2n ** 252n + 27742317777372353535851937790883648493n;
|
|
19
|
+
const P = (1n << 255n) - 19n;
|
|
20
|
+
const Point = ed25519.Point;
|
|
21
|
+
|
|
22
|
+
const toBigLE = bytes => {
|
|
23
|
+
let result = 0n;
|
|
24
|
+
for (let i = bytes.length - 1; i >= 0; i--) result = (result << 8n) | BigInt(bytes[i]);
|
|
25
|
+
return result;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const toLE32 = value => {
|
|
29
|
+
const out = Buffer.alloc(32);
|
|
30
|
+
let n = value;
|
|
31
|
+
for (let i = 0; i < 32; i++) {
|
|
32
|
+
out[i] = Number(n & 255n);
|
|
33
|
+
n >>= 8n;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const sha512 = (...parts) => createHash('sha512').update(Buffer.concat(parts)).digest();
|
|
39
|
+
|
|
40
|
+
const encodePoint = point => Buffer.from(point.toBytes ? point.toBytes() : point.toRawBytes());
|
|
41
|
+
|
|
42
|
+
const powMod = (base, exp, mod) => {
|
|
43
|
+
let result = 1n;
|
|
44
|
+
let b = base % mod;
|
|
45
|
+
let e = exp;
|
|
46
|
+
while (e > 0n) {
|
|
47
|
+
if (e & 1n) result = (result * b) % mod;
|
|
48
|
+
b = (b * b) % mod;
|
|
49
|
+
e >>= 1n;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const CONVERT_CACHE_LIMIT = 4000;
|
|
55
|
+
const convertCache = new Map();
|
|
56
|
+
|
|
57
|
+
// Montgomery u -> Edwards y, yaitu y = (u - 1) / (u + 1). Sama dengan
|
|
58
|
+
// convertPublicKey() pada curve25519-js. Hasil di-cache karena pubkey yang
|
|
59
|
+
// sama dipakai untuk banyak pesan.
|
|
60
|
+
const montgomeryToEdwards = pubKey => {
|
|
61
|
+
const cacheKey = pubKey.toString('base64');
|
|
62
|
+
const cached = convertCache.get(cacheKey);
|
|
63
|
+
if (cached) return cached;
|
|
64
|
+
|
|
65
|
+
const u = toBigLE(pubKey) % P;
|
|
66
|
+
const y = (((u - 1n + P) % P) * powMod((u + 1n) % P, P - 2n, P)) % P;
|
|
67
|
+
const encoded = toLE32(y);
|
|
68
|
+
|
|
69
|
+
if (convertCache.size >= CONVERT_CACHE_LIMIT) convertCache.clear();
|
|
70
|
+
convertCache.set(cacheKey, encoded);
|
|
71
|
+
return encoded;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Mengikuti curve25519_sign() + crypto_sign_direct(): a = clamp(sk), A = a*B,
|
|
75
|
+
// r = H(a || M), R = r*B, S = r + H(R || A || M) * a. Sign bit milik A
|
|
76
|
+
// disalin ke bit tertinggi byte terakhir signature.
|
|
77
|
+
export const calculateSignature = (privKey, message) => {
|
|
78
|
+
const scalar = Buffer.from(privKey);
|
|
79
|
+
scalar[0] &= 248;
|
|
80
|
+
scalar[31] &= 127;
|
|
81
|
+
scalar[31] |= 64;
|
|
82
|
+
|
|
83
|
+
const a = toBigLE(scalar) % L;
|
|
84
|
+
const A = encodePoint(Point.BASE.multiply(a));
|
|
85
|
+
const signBit = A[31] & 128;
|
|
86
|
+
|
|
87
|
+
const msg = Buffer.from(message);
|
|
88
|
+
const r = toBigLE(sha512(scalar, msg)) % L;
|
|
89
|
+
const R = encodePoint(Point.BASE.multiply(r));
|
|
90
|
+
const k = toBigLE(sha512(R, A, msg)) % L;
|
|
91
|
+
const S = (r + k * a) % L;
|
|
92
|
+
|
|
93
|
+
const signature = Buffer.concat([R, toLE32(S)]);
|
|
94
|
+
signature[63] |= signBit;
|
|
95
|
+
return signature;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Mengikuti curve25519_sign_open(): pulihkan sign bit ke public key Edwards,
|
|
99
|
+
// bersihkan bit tersebut dari signature, lalu verifikasi sebagai Ed25519.
|
|
100
|
+
export const verifySignature = (pubKey, message, signature) => {
|
|
101
|
+
// Sama seperti scrubPubKeyFormat() pada libsignal/src/curve.js:
|
|
102
|
+
// public key ber-prefix KEY_BUNDLE_TYPE (0x05) di-strip ke 32 byte murni.
|
|
103
|
+
let rawPub = Buffer.from(pubKey);
|
|
104
|
+
if (rawPub.length === 33 && rawPub[0] === 5) rawPub = rawPub.slice(1);
|
|
105
|
+
if (rawPub.length !== 32) return false;
|
|
106
|
+
|
|
107
|
+
const edPubKey = Buffer.from(montgomeryToEdwards(rawPub));
|
|
108
|
+
edPubKey[31] |= signature[63] & 128;
|
|
109
|
+
|
|
110
|
+
const cleanSignature = Buffer.from(signature);
|
|
111
|
+
cleanSignature[63] &= 127;
|
|
112
|
+
|
|
113
|
+
return ed25519.verify(cleanSignature, Buffer.from(message), edPubKey);
|
|
114
|
+
};
|
|
@@ -59,8 +59,8 @@ export class AbstractSocketClient extends EventEmitter {
|
|
|
59
59
|
* own consumer's teardown; without it the constructor's sink is gone by the time the error lands.
|
|
60
60
|
*/
|
|
61
61
|
removeAllListeners(event) {
|
|
62
|
-
const result =
|
|
63
|
-
if (
|
|
62
|
+
const result = super.removeAllListeners(event);
|
|
63
|
+
if (event === undefined || event === 'error') {
|
|
64
64
|
this.errorSink = undefined;
|
|
65
65
|
this.armErrorSink();
|
|
66
66
|
}
|
|
@@ -73,21 +73,19 @@ export class AbstractSocketClient extends EventEmitter {
|
|
|
73
73
|
return this.on(event, listener);
|
|
74
74
|
}
|
|
75
75
|
once(event, listener) {
|
|
76
|
-
return super.once(event, listener);
|
|
76
|
+
return super.once(event, this.listenerGuard.wrap(listener));
|
|
77
77
|
}
|
|
78
78
|
prependListener(event, listener) {
|
|
79
79
|
return super.prependListener(event, this.listenerGuard.wrap(listener));
|
|
80
80
|
}
|
|
81
81
|
prependOnceListener(event, listener) {
|
|
82
|
-
return super.prependOnceListener(event, listener);
|
|
82
|
+
return super.prependOnceListener(event, this.listenerGuard.wrap(listener));
|
|
83
83
|
}
|
|
84
84
|
off(event, listener) {
|
|
85
|
-
return
|
|
85
|
+
return super.off(event, this.listenerGuard.unwrap(listener));
|
|
86
86
|
}
|
|
87
87
|
removeListener(event, listener) {
|
|
88
|
-
|
|
89
|
-
// the original callback, preserved on the guard's .listener property.
|
|
90
|
-
return super.removeListener(event, listener?.listener ? this.listenerGuard.unwrap(listener) : listener);
|
|
88
|
+
return super.removeListener(event, this.listenerGuard.unwrap(listener));
|
|
91
89
|
}
|
|
92
90
|
}
|
|
93
91
|
//# sourceMappingURL=types.js.map
|
|
@@ -35,51 +35,16 @@ export class WebSocketClient extends AbstractSocketClient {
|
|
|
35
35
|
this.socket?.on(event, (...args) => this.emit(event, ...args));
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
/**
|
|
39
|
-
* Drop the raw socket and open a new one on the SAME emitter. Every stanza listener the layers
|
|
40
|
-
* above registered (messages-recv, chats, the host) stays attached, which is what makes a
|
|
41
|
-
* reconnect invisible to them. The noise session is rebuilt by the caller.
|
|
42
|
-
*/
|
|
43
|
-
reconnect() {
|
|
44
|
-
const socket = this.socket;
|
|
45
|
-
this.socket = null;
|
|
46
|
-
if (socket) {
|
|
47
|
-
// remove the forwarders first: a late close from the old socket must not be reported
|
|
48
|
-
// as a close of the new one
|
|
49
|
-
socket.removeAllListeners();
|
|
50
|
-
try {
|
|
51
|
-
socket.terminate();
|
|
52
|
-
}
|
|
53
|
-
catch { }
|
|
54
|
-
}
|
|
55
|
-
this.connect();
|
|
56
|
-
}
|
|
57
38
|
async close() {
|
|
58
39
|
if (!this.socket) {
|
|
59
40
|
return;
|
|
60
41
|
}
|
|
61
|
-
const socket = this.socket;
|
|
62
|
-
// A half-open peer never answers the close handshake, so an unbounded wait here leaks one
|
|
63
|
-
// socket per reconnect. With a fleet that ends as FD exhaustion, so bound it and terminate.
|
|
64
|
-
const timeoutMs = this.config.closeTimeoutMs ?? 5000;
|
|
65
42
|
const closePromise = new Promise(resolve => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
socket.terminate();
|
|
69
|
-
}
|
|
70
|
-
catch { }
|
|
71
|
-
resolve();
|
|
72
|
-
}, timeoutMs);
|
|
73
|
-
timer.unref?.();
|
|
74
|
-
const done = () => {
|
|
75
|
-
clearTimeout(timer);
|
|
76
|
-
resolve();
|
|
77
|
-
};
|
|
78
|
-
socket.once('close', done);
|
|
79
|
-
socket.once('error', done);
|
|
43
|
+
this.socket?.once('close', resolve);
|
|
44
|
+
this.socket?.once('error', resolve);
|
|
80
45
|
});
|
|
81
46
|
try {
|
|
82
|
-
socket.close();
|
|
47
|
+
this.socket.close();
|
|
83
48
|
} catch { }
|
|
84
49
|
await closePromise;
|
|
85
50
|
this.socket = null;
|
package/lib/Socket/business.d.ts
CHANGED
|
@@ -394,9 +394,6 @@ export function makeBusinessSocket(config: any): {
|
|
|
394
394
|
username: string;
|
|
395
395
|
jid: any;
|
|
396
396
|
exists: boolean;
|
|
397
|
-
keyRequired: boolean;
|
|
398
|
-
isBusiness: boolean;
|
|
399
|
-
pnJid: string | undefined;
|
|
400
397
|
}[]>;
|
|
401
398
|
fetchUsername: (...jids: any[]) => Promise<{
|
|
402
399
|
jid: any;
|
package/lib/Socket/chats.d.ts
CHANGED
|
@@ -142,9 +142,6 @@ export function makeChatsSocket(config: any): {
|
|
|
142
142
|
username: string;
|
|
143
143
|
jid: any;
|
|
144
144
|
exists: boolean;
|
|
145
|
-
keyRequired: boolean;
|
|
146
|
-
isBusiness: boolean;
|
|
147
|
-
pnJid: string | undefined;
|
|
148
145
|
}[]>;
|
|
149
146
|
fetchUsername: (...jids: any[]) => Promise<{
|
|
150
147
|
jid: any;
|
package/lib/Socket/chats.js
CHANGED
|
@@ -65,16 +65,21 @@ export const makeChatsSocket = (config) => {
|
|
|
65
65
|
};
|
|
66
66
|
const fetchPrivacySettings = async (force = false) => {
|
|
67
67
|
if (!privacySettings || force) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
68
|
+
try {
|
|
69
|
+
const { content } = await query({
|
|
70
|
+
tag: 'iq',
|
|
71
|
+
attrs: {
|
|
72
|
+
xmlns: 'privacy',
|
|
73
|
+
to: S_WHATSAPP_NET,
|
|
74
|
+
type: 'get'
|
|
75
|
+
},
|
|
76
|
+
content: [{ tag: 'privacy', attrs: {} }]
|
|
77
|
+
});
|
|
78
|
+
privacySettings = reduceBinaryNodeToDictionary(content?.[0], 'category');
|
|
79
|
+
} catch (error) {
|
|
80
|
+
logger?.error?.({ error }, 'Error fetching privacy settings, falling back to default');
|
|
81
|
+
return { readreceipts: 'all' };
|
|
82
|
+
}
|
|
78
83
|
}
|
|
79
84
|
return privacySettings;
|
|
80
85
|
};
|
|
@@ -953,47 +958,17 @@ export const makeChatsSocket = (config) => {
|
|
|
953
958
|
const executeInitQueries = async () => {
|
|
954
959
|
await Promise.all([fetchProps(), fetchBlocklist(), fetchPrivacySettings()]);
|
|
955
960
|
};
|
|
956
|
-
/**
|
|
957
|
-
* WhatsApp redelivers a message whose ack was lost — the ack goes out after the stanza is
|
|
958
|
-
* processed, so a transport drop in between leaves the server believing nobody took it. The
|
|
959
|
-
* redelivery is indistinguishable from a new message at the stanza level, so the same key.id
|
|
960
|
-
* reached the host twice and every plugin answered twice ("double response" after a reconnect).
|
|
961
|
-
*
|
|
962
|
-
* Per socket, not process-wide: two bots in the same group must each process the group message.
|
|
963
|
-
* Bounded because ids are never reused but this runs for the life of a long-lived socket.
|
|
964
|
-
*/
|
|
965
|
-
const seenMessageIds = new Set();
|
|
966
|
-
const SEEN_MESSAGE_LIMIT = 1000;
|
|
967
|
-
const isRepeatMessage = id => {
|
|
968
|
-
if (!id) {
|
|
969
|
-
return false;
|
|
970
|
-
}
|
|
971
|
-
if (seenMessageIds.has(id)) {
|
|
972
|
-
return true;
|
|
973
|
-
}
|
|
974
|
-
seenMessageIds.add(id);
|
|
975
|
-
if (seenMessageIds.size > SEEN_MESSAGE_LIMIT) {
|
|
976
|
-
seenMessageIds.delete(seenMessageIds.values().next().value);
|
|
977
|
-
}
|
|
978
|
-
return false;
|
|
979
|
-
};
|
|
980
961
|
const upsertMessage = ev.createBufferedFunction(async (msg, type) => {
|
|
981
|
-
if (isRepeatMessage(msg.key?.id)) {
|
|
982
|
-
logger.debug({ msgId: msg.key?.id, type }, 'dropped redelivered message');
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
962
|
ev.emit('messages.upsert', { messages: [msg], type });
|
|
986
963
|
if (!!msg.pushName) {
|
|
987
|
-
let jid = msg.key.fromMe ? authState.creds.me
|
|
964
|
+
let jid = msg.key.fromMe ? authState.creds.me.id : msg.key.participant || msg.key.remoteJid;
|
|
988
965
|
jid = jidNormalizedUser(jid);
|
|
989
966
|
if (!msg.key.fromMe) {
|
|
990
967
|
ev.emit('contacts.update', [{ id: jid, notify: msg.pushName, verifiedName: msg.verifiedBizName }]);
|
|
991
968
|
}
|
|
992
969
|
// update our pushname too
|
|
993
970
|
if (msg.key.fromMe && msg.pushName && authState.creds.me?.name !== msg.pushName) {
|
|
994
|
-
|
|
995
|
-
ev.emit('creds.update', { me: { ...authState.creds.me, name: msg.pushName } });
|
|
996
|
-
}
|
|
971
|
+
ev.emit('creds.update', { me: { ...authState.creds.me, name: msg.pushName } });
|
|
997
972
|
}
|
|
998
973
|
}
|
|
999
974
|
const historyMsg = getHistoryMsg(msg.message);
|
|
@@ -1107,9 +1082,7 @@ export const makeChatsSocket = (config) => {
|
|
|
1107
1082
|
if (attrs.timestamp) {
|
|
1108
1083
|
let { lastAccountSyncTimestamp } = authState.creds;
|
|
1109
1084
|
if (lastAccountSyncTimestamp) {
|
|
1110
|
-
|
|
1111
|
-
await cleanDirtyBits('account_sync', lastAccountSyncTimestamp)
|
|
1112
|
-
.catch(error => logger.warn({ error }, 'failed to clean account_sync dirty bits'));
|
|
1085
|
+
await cleanDirtyBits('account_sync', lastAccountSyncTimestamp);
|
|
1113
1086
|
}
|
|
1114
1087
|
lastAccountSyncTimestamp = +attrs.timestamp;
|
|
1115
1088
|
ev.emit('creds.update', { lastAccountSyncTimestamp });
|
|
@@ -539,9 +539,6 @@ export function makeCommunitiesSocket(config: any): {
|
|
|
539
539
|
username: string;
|
|
540
540
|
jid: any;
|
|
541
541
|
exists: boolean;
|
|
542
|
-
keyRequired: boolean;
|
|
543
|
-
isBusiness: boolean;
|
|
544
|
-
pnJid: string | undefined;
|
|
545
542
|
}[]>;
|
|
546
543
|
fetchUsername: (...jids: any[]) => Promise<{
|
|
547
544
|
jid: any;
|