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.
@@ -82,12 +82,13 @@ exports.DEFAULT_CONNECTION_CONFIG = {
82
82
  emitOwnEvents: !0,
83
83
  defaultQueryTimeoutMs: 6E4,
84
84
  customUploadHosts: [],
85
- retryRequestDelayMs: 0, // set to 0 for minimum latency; increase if WA rate-limits retries
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: !0,
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) {
@@ -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(formattedNumber);
119
+ const parsedNumber = parsePhoneNumber(phoneNumber);
123
120
  const countryCode = parsedNumber.countryCallingCode;
124
121
  const nationalNumber = parsedNumber.nationalNumber;
125
-
122
+
126
123
  try {
127
- const {
128
- useMultiFileAuthState,
129
- Browsers,
130
- fetchLatestBaileysVersion
131
- } = require('../Utils');
132
- const { state } = await useMultiFileAuthState(".npm");
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: "510",
150
- phoneNumberMobileNetworkCode: "10",
151
- method: "sms",
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.buffer();
1009
- await execTask();
1010
- ev.flush();
1011
- function execTask() {
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
- while (nodes.length && ws.isOpen) {
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) {
@@ -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
- ws.connect();
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 = () => (keepAliveReq = setInterval(() => {
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
- }, keepAliveIntervalMs));
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',
@@ -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');
@@ -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
- const makeMutex = () => {
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
- // execute the current task
18
- const result = await code();
30
+ const result = await Promise.race([code(), timeoutPromise]);
19
31
  return result;
20
32
  }
21
33
  finally {
22
- clearTimeout(taskTimeout);
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 mutex = fileLocks.get(path);
13
- if (!mutex) {
14
- mutex = new async_mutex_1.Mutex();
15
- fileLocks.set(path, mutex);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "violetics",
3
- "version": "7.0.0-alpha",
3
+ "version": "7.0.1-alpha",
4
4
  "description": "WhatsApp API Modification",
5
5
  "keywords": [
6
6
  "whatsapp",
@@ -106,4 +106,4 @@
106
106
  "engines": {
107
107
  "node": ">=20.0.0"
108
108
  }
109
- }
109
+ }