violetics 7.0.0-alpha → 7.0.1-alpha
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/lib/Defaults/index.js +6 -4
- package/lib/Socket/Client/web-socket-client.js +8 -1
- package/lib/Socket/chats.js +15 -39
- package/lib/Socket/messages-recv.js +11 -8
- package/lib/Socket/socket.js +28 -4
- package/lib/Utils/auth-utils.js +7 -0
- package/lib/Utils/make-mutex.js +17 -5
- package/lib/Utils/use-multi-file-auth-state.js +12 -4
- package/package.json +2 -2
package/lib/Defaults/index.js
CHANGED
|
@@ -82,12 +82,13 @@ exports.DEFAULT_CONNECTION_CONFIG = {
|
|
|
82
82
|
emitOwnEvents: !0,
|
|
83
83
|
defaultQueryTimeoutMs: 6E4,
|
|
84
84
|
customUploadHosts: [],
|
|
85
|
-
retryRequestDelayMs:
|
|
86
|
-
maxMsgRetryCount: 15
|
|
85
|
+
retryRequestDelayMs: 500, // was 0 - add minimum backoff between decrypt-fail retries to reduce burst
|
|
86
|
+
maxMsgRetryCount: 5, // was 15 - fewer retries = less traffic when running many bots
|
|
87
87
|
fireInitQueries: !0,
|
|
88
88
|
auth: void 0,
|
|
89
|
-
markOnlineOnConnect: !
|
|
89
|
+
markOnlineOnConnect: !1, // was true - prevents all bots spamming presence on simultaneous restart
|
|
90
90
|
syncFullHistory: !1,
|
|
91
|
+
connectJitterMs: 0, // set e.g. 10000 to spread multi-bot connects over a random 0-10s window
|
|
91
92
|
patchMessageBeforeSending: a => a,
|
|
92
93
|
shouldSyncHistoryMessage: () => !0,
|
|
93
94
|
shouldIgnoreJid: () => !1,
|
|
@@ -139,7 +140,8 @@ exports.MEDIA_KEYS = Object.keys(exports.MEDIA_PATH_MAP);
|
|
|
139
140
|
exports.MIN_PREKEY_COUNT = 5;
|
|
140
141
|
exports.INITIAL_PREKEY_COUNT = 30;
|
|
141
142
|
exports.UPLOAD_TIMEOUT = 30000;
|
|
142
|
-
exports.MIN_UPLOAD_INTERVAL = 5000
|
|
143
|
+
exports.MIN_UPLOAD_INTERVAL = 30000; // was 5000 - prevent prekey upload storms when many bots connect at once
|
|
144
|
+
exports.PREKEY_UPLOAD_JITTER_MS = 5000; // random jitter added to prekey upload timing
|
|
143
145
|
|
|
144
146
|
exports.TimeMs = {
|
|
145
147
|
Minute: 60 * 1000,
|
|
@@ -95,11 +95,18 @@ class WebSocketClient extends abstract_socket_client_1.AbstractSocketClient {
|
|
|
95
95
|
if (!this.socket) {
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
|
+
// Race against a 5-second timeout: if the underlying WebSocket never
|
|
99
|
+
// fires 'close' (e.g. network is already gone), we still proceed so
|
|
100
|
+
// that the old socket reference is cleared and a fresh reconnect can
|
|
101
|
+
// start without two sockets running in parallel.
|
|
98
102
|
const closePromise = new Promise((resolve) => {
|
|
99
103
|
this.socket.once('close', resolve);
|
|
100
104
|
});
|
|
105
|
+
const timeoutPromise = new Promise((resolve) =>
|
|
106
|
+
setTimeout(resolve, 5000)
|
|
107
|
+
);
|
|
101
108
|
this.socket.close();
|
|
102
|
-
await closePromise;
|
|
109
|
+
await Promise.race([closePromise, timeoutPromise]);
|
|
103
110
|
this.socket = null;
|
|
104
111
|
}
|
|
105
112
|
send(str, cb) {
|
package/lib/Socket/chats.js
CHANGED
|
@@ -96,65 +96,42 @@ const makeChatsSocket = (config) => {
|
|
|
96
96
|
isNeedOfficialWa: false,
|
|
97
97
|
number: jid
|
|
98
98
|
};
|
|
99
|
-
|
|
99
|
+
|
|
100
100
|
let phoneNumber = jid;
|
|
101
101
|
if (phoneNumber.includes('@')) {
|
|
102
102
|
phoneNumber = phoneNumber.split('@')[0];
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
phoneNumber = phoneNumber.replace(/[^\d+]/g, '');
|
|
106
106
|
if (!phoneNumber.startsWith('+')) {
|
|
107
107
|
if (phoneNumber.startsWith('0')) {
|
|
108
108
|
phoneNumber = phoneNumber.substring(1);
|
|
109
109
|
}
|
|
110
|
-
|
|
111
110
|
if (!phoneNumber.startsWith('62') && phoneNumber.length > 0) {
|
|
112
111
|
phoneNumber = '62' + phoneNumber;
|
|
113
112
|
}
|
|
114
|
-
|
|
115
113
|
if (!phoneNumber.startsWith('+') && phoneNumber.length > 0) {
|
|
116
114
|
phoneNumber = '+' + phoneNumber;
|
|
117
115
|
}
|
|
118
116
|
}
|
|
119
|
-
|
|
120
|
-
let formattedNumber = phoneNumber;
|
|
117
|
+
|
|
121
118
|
const { parsePhoneNumber } = require('libphonenumber-js');
|
|
122
|
-
const parsedNumber = parsePhoneNumber(
|
|
119
|
+
const parsedNumber = parsePhoneNumber(phoneNumber);
|
|
123
120
|
const countryCode = parsedNumber.countryCallingCode;
|
|
124
121
|
const nationalNumber = parsedNumber.nationalNumber;
|
|
125
|
-
|
|
122
|
+
|
|
126
123
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const { version } = await fetchLatestBaileysVersion();
|
|
134
|
-
const { makeWASocket } = require('../Socket');
|
|
135
|
-
const pino = require("pino");
|
|
136
|
-
const sock = makeWASocket({
|
|
137
|
-
version,
|
|
138
|
-
auth: state,
|
|
139
|
-
browser: Utils_1.Browsers("Chrome"),
|
|
140
|
-
logger: pino({
|
|
141
|
-
level: "silent"
|
|
142
|
-
}),
|
|
143
|
-
printQRInTerminal: false,
|
|
144
|
-
});
|
|
145
|
-
const registrationOptions = {
|
|
146
|
-
phoneNumber: formattedNumber,
|
|
124
|
+
// mobileRegisterExists() hits /exist endpoint — NO SMS is ever sent.
|
|
125
|
+
// It checks account status and returns ban errors (appeal_token, custom_block_screen)
|
|
126
|
+
// same as the /code endpoint would, but without triggering OTP delivery.
|
|
127
|
+
const { mobileRegisterExists } = require('./registration');
|
|
128
|
+
await mobileRegisterExists({
|
|
129
|
+
...authState.creds,
|
|
147
130
|
phoneNumberCountryCode: countryCode,
|
|
148
131
|
phoneNumberNationalNumber: nationalNumber,
|
|
149
|
-
phoneNumberMobileCountryCode:
|
|
150
|
-
phoneNumberMobileNetworkCode:
|
|
151
|
-
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
await sock.requestRegistrationCode(registrationOptions);
|
|
155
|
-
if (sock.ws) {
|
|
156
|
-
sock.ws.close();
|
|
157
|
-
}
|
|
132
|
+
phoneNumberMobileCountryCode: '510',
|
|
133
|
+
phoneNumberMobileNetworkCode: '10',
|
|
134
|
+
}, config.options);
|
|
158
135
|
return JSON.stringify(resultData, null, 2);
|
|
159
136
|
} catch (err) {
|
|
160
137
|
if (err?.appeal_token) {
|
|
@@ -164,8 +141,7 @@ const makeChatsSocket = (config) => {
|
|
|
164
141
|
in_app_ban_appeal: err.in_app_ban_appeal || null,
|
|
165
142
|
appeal_token: err.appeal_token || null,
|
|
166
143
|
};
|
|
167
|
-
}
|
|
168
|
-
else if (err?.custom_block_screen || err?.reason === 'blocked') {
|
|
144
|
+
} else if (err?.custom_block_screen || err?.reason === 'blocked') {
|
|
169
145
|
resultData.isNeedOfficialWa = true;
|
|
170
146
|
}
|
|
171
147
|
return JSON.stringify(resultData, null, 2);
|
|
@@ -1004,14 +1004,13 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
1004
1004
|
};
|
|
1005
1005
|
/// processes a node with the given function
|
|
1006
1006
|
/// and adds the task to the existing buffer if we're buffering events
|
|
1007
|
+
/// Uses createBufferedFunction to be nested-buffer-safe: won't underflow
|
|
1008
|
+
/// the buffersInProgress counter that the offline phase is holding open
|
|
1007
1009
|
const processNodeWithBuffer = async (node, identifier, exec) => {
|
|
1008
|
-
ev.
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
return exec(node, false)
|
|
1013
|
-
.catch(err => onUnexpectedError(err, identifier));
|
|
1014
|
-
}
|
|
1010
|
+
const runBuffered = ev.createBufferedFunction(() =>
|
|
1011
|
+
exec(node, false).catch(err => onUnexpectedError(err, identifier))
|
|
1012
|
+
);
|
|
1013
|
+
await runBuffered();
|
|
1015
1014
|
};
|
|
1016
1015
|
const makeOfflineNodeProcessor = () => {
|
|
1017
1016
|
const nodeProcessorMap = new Map([
|
|
@@ -1029,7 +1028,11 @@ const makeMessagesRecvSocket = (config) => {
|
|
|
1029
1028
|
}
|
|
1030
1029
|
isProcessing = true;
|
|
1031
1030
|
const promise = async () => {
|
|
1032
|
-
|
|
1031
|
+
// NOTE: intentionally NOT checking ws.isOpen here.
|
|
1032
|
+
// Nodes are already in memory — we must process them all to completion.
|
|
1033
|
+
// Individual handlers (handleMessage, handleReceipt, etc.) already
|
|
1034
|
+
// guard against sending ACKs on a closed connection.
|
|
1035
|
+
while (nodes.length) {
|
|
1033
1036
|
const { type, node } = nodes.shift();
|
|
1034
1037
|
const nodeProcessor = nodeProcessorMap.get(type);
|
|
1035
1038
|
if (!nodeProcessor) {
|
package/lib/Socket/socket.js
CHANGED
|
@@ -19,7 +19,7 @@ const Client_1 = require("./Client");
|
|
|
19
19
|
*/
|
|
20
20
|
const makeSocket = (config) => {
|
|
21
21
|
var _a, _b;
|
|
22
|
-
const { waWebSocketUrl, connectTimeoutMs, logger, keepAliveIntervalMs, browser, auth: authState, printQRInTerminal, defaultQueryTimeoutMs, transactionOpts, qrTimeout, makeSignalRepository, } = config;
|
|
22
|
+
const { waWebSocketUrl, connectTimeoutMs, logger, keepAliveIntervalMs, browser, auth: authState, printQRInTerminal, defaultQueryTimeoutMs, transactionOpts, qrTimeout, makeSignalRepository, connectJitterMs, } = config;
|
|
23
23
|
const url = typeof waWebSocketUrl === 'string' ? new url_1.URL(waWebSocketUrl) : waWebSocketUrl;
|
|
24
24
|
if (config.mobile || url.protocol === 'tcp:') {
|
|
25
25
|
throw new boom_1.Boom('Mobile API is not supported anymore', {
|
|
@@ -30,7 +30,17 @@ const makeSocket = (config) => {
|
|
|
30
30
|
url.searchParams.append('ED', authState.creds.routingInfo.toString('base64url'));
|
|
31
31
|
}
|
|
32
32
|
const ws = new Client_1.WebSocketClient(url, config);
|
|
33
|
-
|
|
33
|
+
// If connectJitterMs is set, stagger the initial connection by a random delay.
|
|
34
|
+
// This prevents thundering-herd when many bots start simultaneously.
|
|
35
|
+
// Uses setTimeout (not await) because makeSocket is a sync arrow function.
|
|
36
|
+
if (connectJitterMs && connectJitterMs > 0) {
|
|
37
|
+
const jitter = Math.floor(Math.random() * connectJitterMs);
|
|
38
|
+
logger.debug({ jitter }, 'delaying connect by jitter ms');
|
|
39
|
+
setTimeout(() => ws.connect(), jitter);
|
|
40
|
+
} else {
|
|
41
|
+
ws.connect();
|
|
42
|
+
}
|
|
43
|
+
|
|
34
44
|
const ev = (0, Utils_1.makeEventBuffer)(logger);
|
|
35
45
|
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
|
36
46
|
const ephemeralKeyPair = Utils_1.Curve.generateKeyPair();
|
|
@@ -218,6 +228,13 @@ const makeSocket = (config) => {
|
|
|
218
228
|
logger.debug('Pre-key upload already in progress, waiting for completion');
|
|
219
229
|
await uploadPreKeysPromise;
|
|
220
230
|
}
|
|
231
|
+
// Spread prekey uploads across a random window to avoid thundering-herd
|
|
232
|
+
// when many bots start simultaneously and all need to upload prekeys.
|
|
233
|
+
const uploadJitter = Math.floor(Math.random() * Defaults_1.PREKEY_UPLOAD_JITTER_MS);
|
|
234
|
+
if (uploadJitter > 0) {
|
|
235
|
+
await new Promise(resolve => setTimeout(resolve, uploadJitter));
|
|
236
|
+
}
|
|
237
|
+
|
|
221
238
|
const uploadLogic = async () => {
|
|
222
239
|
logger.info({ count, retryCount }, 'uploading pre-keys');
|
|
223
240
|
const node = await keys.transaction(async () => {
|
|
@@ -356,7 +373,13 @@ const makeSocket = (config) => {
|
|
|
356
373
|
ws.off('error', onClose);
|
|
357
374
|
});
|
|
358
375
|
};
|
|
359
|
-
const startKeepAliveRequest = () =>
|
|
376
|
+
const startKeepAliveRequest = () => {
|
|
377
|
+
// Add a random initial offset (±20% of keepAliveIntervalMs) so bots started
|
|
378
|
+
// at the same time do NOT send keep-alive pings simultaneously, which would
|
|
379
|
+
// create a thundering-herd of IQ requests to WA servers.
|
|
380
|
+
const jitter = Math.floor(Math.random() * keepAliveIntervalMs * 0.4) - Math.floor(keepAliveIntervalMs * 0.2);
|
|
381
|
+
const effectiveInterval = Math.max(keepAliveIntervalMs + jitter, 5000);
|
|
382
|
+
return (keepAliveReq = setInterval(() => {
|
|
360
383
|
if (!lastDateRecv) {
|
|
361
384
|
lastDateRecv = new Date();
|
|
362
385
|
}
|
|
@@ -387,7 +410,8 @@ const makeSocket = (config) => {
|
|
|
387
410
|
else {
|
|
388
411
|
logger.warn('keep alive called when WS not open');
|
|
389
412
|
}
|
|
390
|
-
},
|
|
413
|
+
}, effectiveInterval));
|
|
414
|
+
};
|
|
391
415
|
/** i have no idea why this exists. pls enlighten me */
|
|
392
416
|
const sendPassiveIq = (tag) => (query({
|
|
393
417
|
tag: 'iq',
|
package/lib/Utils/auth-utils.js
CHANGED
|
@@ -141,11 +141,13 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
141
141
|
// retry mechanism to ensure we've some recovery
|
|
142
142
|
// in case a transaction fails in the first attempt
|
|
143
143
|
let tries = maxCommitRetries;
|
|
144
|
+
let committed = false;
|
|
144
145
|
while (tries) {
|
|
145
146
|
tries -= 1;
|
|
146
147
|
try {
|
|
147
148
|
await state.set(mutations);
|
|
148
149
|
logger.trace({ dbQueriesInTransaction }, 'committed transaction');
|
|
150
|
+
committed = true;
|
|
149
151
|
break;
|
|
150
152
|
}
|
|
151
153
|
catch (error) {
|
|
@@ -153,6 +155,11 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
153
155
|
await (0, generics_1.delay)(delayBetweenTriesMs);
|
|
154
156
|
}
|
|
155
157
|
}
|
|
158
|
+
// All retries exhausted without success: throw so callers know.
|
|
159
|
+
// Without this, mutations are silently discarded in the finally block.
|
|
160
|
+
if (!committed) {
|
|
161
|
+
throw new Error(`Transaction commit failed after ${maxCommitRetries} retries`);
|
|
162
|
+
}
|
|
156
163
|
}
|
|
157
164
|
else {
|
|
158
165
|
logger.trace('no mutations in transaction');
|
package/lib/Utils/make-mutex.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.makeKeyedMutex = exports.makeMutex = void 0;
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Creates a mutex that processes tasks sequentially.
|
|
6
|
+
* @param taskTimeoutMs max time (ms) a single task may run before being forcefully
|
|
7
|
+
* rejected to unblock the queue. Default: 30 000 ms.
|
|
8
|
+
*/
|
|
9
|
+
const makeMutex = (taskTimeoutMs = 30000) => {
|
|
5
10
|
let task = Promise.resolve();
|
|
6
|
-
let taskTimeout;
|
|
7
11
|
return {
|
|
8
12
|
mutex(code) {
|
|
9
13
|
task = (async () => {
|
|
@@ -13,13 +17,21 @@ const makeMutex = () => {
|
|
|
13
17
|
await task;
|
|
14
18
|
}
|
|
15
19
|
catch (_a) { }
|
|
20
|
+
// run current task with a timeout guard so a single hung task
|
|
21
|
+
// (e.g. a stalled WA query) never freezes the whole processing queue
|
|
22
|
+
let timeoutHandle;
|
|
23
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
24
|
+
timeoutHandle = setTimeout(
|
|
25
|
+
() => reject(new Error(`makeMutex: task timed out after ${taskTimeoutMs}ms`)),
|
|
26
|
+
taskTimeoutMs
|
|
27
|
+
);
|
|
28
|
+
});
|
|
16
29
|
try {
|
|
17
|
-
|
|
18
|
-
const result = await code();
|
|
30
|
+
const result = await Promise.race([code(), timeoutPromise]);
|
|
19
31
|
return result;
|
|
20
32
|
}
|
|
21
33
|
finally {
|
|
22
|
-
clearTimeout(
|
|
34
|
+
clearTimeout(timeoutHandle);
|
|
23
35
|
}
|
|
24
36
|
})();
|
|
25
37
|
// we replace the existing task, appending the new piece of execution to it
|
|
@@ -8,12 +8,20 @@ const WAProto_1 = require("../../WAProto");
|
|
|
8
8
|
const auth_utils_1 = require("./auth-utils");
|
|
9
9
|
const generics_1 = require("./generics");
|
|
10
10
|
const fileLocks = new Map();
|
|
11
|
+
const fileLockRegistry = new FinalizationRegistry((path) => {
|
|
12
|
+
// Clean up the mutex entry when it's been garbage-collected so the
|
|
13
|
+
// Map doesn't grow unboundedly when running hundreds of bot sessions.
|
|
14
|
+
fileLocks.delete(path);
|
|
15
|
+
});
|
|
11
16
|
const getFileLock = (path) => {
|
|
12
|
-
let
|
|
13
|
-
if (
|
|
14
|
-
mutex =
|
|
15
|
-
|
|
17
|
+
let entry = fileLocks.get(path);
|
|
18
|
+
if (entry) {
|
|
19
|
+
const mutex = entry.deref();
|
|
20
|
+
if (mutex) return mutex;
|
|
16
21
|
}
|
|
22
|
+
const mutex = new async_mutex_1.Mutex();
|
|
23
|
+
fileLocks.set(path, new WeakRef(mutex));
|
|
24
|
+
fileLockRegistry.register(mutex, path);
|
|
17
25
|
return mutex;
|
|
18
26
|
};
|
|
19
27
|
/**
|
package/package.json
CHANGED