@raevon/n8n-nodes-whatsapp 1.0.13 → 2.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.
@@ -1,4 +1,3 @@
1
- import { type WASocket } from '@whiskeysockets/baileys';
2
1
  export interface WhatsAppCredentials {
3
2
  sessionPath: string;
4
3
  messageDelayMinMs: number;
@@ -10,56 +9,8 @@ export interface WhatsAppCredentials {
10
9
  dailySendLimit: number;
11
10
  checkRecipientExists: boolean;
12
11
  }
13
- interface AntiBanConfig {
14
- messageDelayMinMs: number;
15
- messageDelayMaxMs: number;
16
- burstSize: number;
17
- burstPauseMinMs: number;
18
- burstPauseMaxMs: number;
19
- typingSimulation: boolean;
20
- dailySendLimit: number;
21
- checkRecipientExists: boolean;
22
- }
23
- export declare function getWhatsAppCredentials(credentials: Record<string, any>): Promise<WhatsAppCredentials>;
24
- export declare function ensureConnected(cfg: WhatsAppCredentials): Promise<WASocket>;
25
- export declare function connectOrGetQr(cfg: WhatsAppCredentials): Promise<{
26
- connected: boolean;
27
- qrUrl?: string;
28
- message: string;
29
- }>;
30
- export declare function simulateTyping(jid: string, content: {
31
- text?: string;
32
- caption?: string;
33
- }, cfg: AntiBanConfig): Promise<void>;
34
- export declare function sendMessageWithAntiBan(to: string, content: {
35
- text?: string;
36
- image?: Buffer;
37
- document?: Buffer;
38
- audio?: Buffer;
39
- location?: {
40
- degreesLatitude: number;
41
- degreesLongitude: number;
42
- name?: string;
43
- address?: string;
44
- };
45
- mimetype?: string;
46
- fileName?: string;
47
- caption?: string;
48
- ptt?: boolean;
49
- }, cfg: WhatsAppCredentials): Promise<{
50
- messageId: string;
51
- status: string;
52
- recipient: string;
53
- }>;
54
- export declare function getConnectionStatus(): {
55
- status: string;
56
- connected: boolean;
57
- qrAvailable: boolean;
58
- queueSize: number;
59
- sentToday: number;
60
- dailyLimit: number;
61
- lastError: string | null;
62
- };
12
+ export declare function getServerUrl(cfg: WhatsAppCredentials): string;
13
+ export declare function ensureServerRunning(cfg: WhatsAppCredentials): Promise<void>;
63
14
  export declare function disconnect(): Promise<void>;
64
15
  export declare function parseIncomingMessage(msg: any): Record<string, any> | null;
65
- export {};
16
+ export declare function getWhatsAppCredentials(credentials: Record<string, any>): Promise<WhatsAppCredentials>;
@@ -1,504 +1,99 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
4
  };
38
5
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.getWhatsAppCredentials = getWhatsAppCredentials;
40
- exports.ensureConnected = ensureConnected;
41
- exports.connectOrGetQr = connectOrGetQr;
42
- exports.simulateTyping = simulateTyping;
43
- exports.sendMessageWithAntiBan = sendMessageWithAntiBan;
44
- exports.getConnectionStatus = getConnectionStatus;
6
+ exports.getServerUrl = getServerUrl;
7
+ exports.ensureServerRunning = ensureServerRunning;
45
8
  exports.disconnect = disconnect;
46
9
  exports.parseIncomingMessage = parseIncomingMessage;
47
- const n8n_workflow_1 = require("n8n-workflow");
48
- const baileys_1 = __importStar(require("@whiskeysockets/baileys"));
49
- const p_queue_1 = __importDefault(require("p-queue"));
10
+ exports.getWhatsAppCredentials = getWhatsAppCredentials;
11
+ const node_child_process_1 = require("node:child_process");
50
12
  const node_path_1 = __importDefault(require("node:path"));
51
13
  const node_fs_1 = __importDefault(require("node:fs"));
52
- const qrcode_1 = __importDefault(require("qrcode"));
53
- const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
54
- const randBetween = (min, max) => max > min ? min + Math.floor(Math.random() * (max - min + 1)) : min;
55
- // Silent logger — Baileys expects pino-compatible logger with these methods
56
- const noop = () => { };
57
- const silentLogger = {
58
- level: 'silent',
59
- info: noop,
60
- warn: noop,
61
- error: noop,
62
- debug: noop,
63
- fatal: noop,
64
- trace: noop,
65
- child: () => silentLogger,
66
- };
67
- // I8: Extracted ~ path expansion to avoid duplication
14
+ const node_net_1 = __importDefault(require("node:net"));
68
15
  function expandHome(p) {
69
16
  if (!p.startsWith('~'))
70
17
  return p;
71
18
  return node_path_1.default.join(process.env.HOME || process.env.USERPROFILE || '', p.slice(1));
72
19
  }
73
- // --- Singleton socket manager ---
74
- // NOTE: This singleton is shared across ALL n8n workflow executions using the same credential.
75
- // Anti-ban counters (sentTodayCount, sentInBurst, nextSendAt) are module-level two workflows
76
- // burning through the same daily limit will share the counter. This is intentional for a
77
- // single-credential setup. Multi-credential support would require a Map<sessionPath, State>.
78
- let socketInstance = null;
79
- let socketStatus = 'stopped';
80
- let socketConfig = null;
81
- let sessionPath = null;
82
- let queue = null;
83
- let nextSendAt = 0;
84
- let sentInBurst = 0;
85
- let sentTodayCount = 0;
86
- let sentTodayDate = '';
87
- let reconnectAttempts = 0;
88
- let reconnectTimer = null;
89
- let qrResolve = null;
90
- let latestQr = null;
91
- let generation = 0; // #10: Generation counter — prevents stale handlers on reconnect
92
- let credsSavedPromise = null; // Track credential save completion
93
- let lastDisconnectError = null; // Track last disconnect reason for output
94
- function todayStartIso() {
95
- return new Date().toISOString().slice(0, 10) + 'T00:00:00.000Z';
20
+ function getPort(cfg) {
21
+ // Derive port from session path to avoid conflicts between credentials
22
+ const hash = cfg.sessionPath.split('').reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0);
23
+ return 3456 + (Math.abs(hash) % 1000);
96
24
  }
97
- function sendGapMs(cfg) {
98
- return randBetween(cfg.messageDelayMinMs, cfg.messageDelayMaxMs);
25
+ function getServerUrl(cfg) {
26
+ return `http://127.0.0.1:${getPort(cfg)}`;
99
27
  }
100
- function burstPauseMs(cfg) {
101
- const size = cfg.burstSize;
102
- if (!size || ++sentInBurst < size)
103
- return 0;
104
- sentInBurst = 0;
105
- return randBetween(cfg.burstPauseMinMs, cfg.burstPauseMaxMs);
106
- }
107
- function normalizeRecipient(to) {
108
- if (to.endsWith('@s.whatsapp.net') || to.endsWith('@g.us'))
109
- return to;
110
- const digits = to.replace(/\D/g, '');
111
- if (!/^[1-9][0-9]{7,14}$/.test(digits)) {
112
- throw new Error(`Invalid phone number: ${to}`);
113
- }
114
- return `${digits}@s.whatsapp.net`;
28
+ async function isPortOpen(port) {
29
+ return new Promise((resolve) => {
30
+ const server = node_net_1.default.createServer();
31
+ server.once('error', () => resolve(false));
32
+ server.once('listening', () => { server.close(); resolve(true); });
33
+ server.listen(port, '127.0.0.1');
34
+ });
115
35
  }
116
- // #7: Reconnect backoff with jitter — exponential 1s→60s, ±20% randomization
117
- async function scheduleReconnect(cfg) {
118
- if (reconnectTimer)
36
+ let serverProcess = null;
37
+ let serverPort = 0;
38
+ async function ensureServerRunning(cfg) {
39
+ const port = getPort(cfg);
40
+ // Check if server is already running
41
+ if (await isPortOpen(port)) {
42
+ serverPort = port;
119
43
  return;
120
- const base = Math.min(1000 * (2 ** reconnectAttempts), 60000);
121
- const delay = Math.round(base * (0.8 + Math.random() * 0.4));
122
- reconnectAttempts += 1;
123
- reconnectTimer = setTimeout(() => {
124
- reconnectTimer = null;
125
- initSocket(cfg, sessionPath).catch(() => { });
126
- }, delay);
127
- }
128
- async function initSocket(cfg, authPath) {
129
- // Always create fresh socket if current one is dead
130
- if (socketInstance && socketStatus === 'connected') {
131
- try {
132
- if (socketInstance.user)
133
- return socketInstance;
134
- }
135
- catch {
136
- // Socket dead, create new one
137
- }
138
- }
139
- const gen = ++generation; // #10: Snapshot generation for this connection attempt
140
- const resolvedPath = expandHome(authPath);
141
- if (!node_fs_1.default.existsSync(resolvedPath)) {
142
- node_fs_1.default.mkdirSync(resolvedPath, { recursive: true });
143
44
  }
144
- const { state, saveCreds } = await (0, baileys_1.useMultiFileAuthState)(resolvedPath);
145
- const { version } = await (0, baileys_1.fetchLatestBaileysVersion)();
146
- const sock = (0, baileys_1.default)({
147
- version,
148
- // #12: Browser fingerprint — appears as Ubuntu Firefox, not a bot
149
- browser: baileys_1.Browsers.ubuntu('n8n WhatsApp Node'),
150
- auth: {
151
- creds: state.creds,
152
- keys: (0, baileys_1.makeCacheableSignalKeyStore)(state.keys, silentLogger),
153
- },
154
- // #11: Stealth flags — don't go online, don't sync history, no link previews
155
- markOnlineOnConnect: false,
156
- syncFullHistory: false,
157
- shouldSyncHistoryMessage: () => false,
158
- generateHighQualityLinkPreview: false,
159
- logger: silentLogger,
160
- });
161
- socketInstance = sock;
162
- socketStatus = 'connecting';
163
- reconnectAttempts = 0;
164
- sock.ev.on('creds.update', () => {
165
- if (gen !== generation)
166
- return; // #10: Ignore stale connection events
167
- // Track credential save completion — ensures session is persisted before execution ends
168
- credsSavedPromise = saveCreds().catch((err) => {
169
- console.error('[WhatsApp] Failed to save credentials:', err.message);
170
- });
171
- });
172
- sock.ev.on('connection.update', async (update) => {
173
- var _a, _b, _c;
174
- if (gen !== generation)
175
- return; // #10: Ignore stale connection events
176
- const { connection, lastDisconnect, qr } = update;
177
- if (qr) {
178
- latestQr = qr;
179
- socketStatus = 'qr_ready';
180
- // Generate QR locally using qrcode package — no external service
181
- if (qrResolve) {
182
- const qrDataUrl = await qrcode_1.default.toDataURL(qr, { width: 300, margin: 2 });
183
- qrResolve({ qr, qrUrl: qrDataUrl });
184
- qrResolve = null;
185
- }
186
- }
187
- if (connection === 'open') {
188
- socketStatus = 'connected';
189
- reconnectAttempts = 0;
190
- latestQr = null;
191
- lastDisconnectError = null;
192
- return;
193
- }
194
- if (connection === 'close') {
195
- const code = (_b = (_a = lastDisconnect === null || lastDisconnect === void 0 ? void 0 : lastDisconnect.error) === null || _a === void 0 ? void 0 : _a.output) === null || _b === void 0 ? void 0 : _b.statusCode;
196
- const errorMsg = ((_c = lastDisconnect === null || lastDisconnect === void 0 ? void 0 : lastDisconnect.error) === null || _c === void 0 ? void 0 : _c.message) || 'Unknown error';
197
- const reason = baileys_1.DisconnectReason[code] || 'Unknown';
198
- // Store error for output in nodes
199
- lastDisconnectError = `Code: ${code} (${reason}), Error: ${errorMsg}`;
200
- if (code === baileys_1.DisconnectReason.loggedOut) {
201
- socketStatus = 'logged_out';
202
- socketInstance = null;
203
- ++generation;
204
- }
205
- else {
206
- socketStatus = 'disconnected';
207
- socketInstance = null;
208
- // Don't auto-reconnect if we're in a loop — let the node handle it
209
- if (reconnectAttempts < 3) {
210
- scheduleReconnect(cfg);
211
- }
212
- }
213
- }
214
- });
215
- // #8: Fire-and-forget — no receipt tracking, no delivery acks, minimal protocol chatter
216
- return sock;
217
- }
218
- // --- Public API ---
219
- async function getWhatsAppCredentials(credentials) {
220
- var _a, _b;
221
- return {
222
- sessionPath: credentials.sessionPath || '~/.n8n/whatsapp-auth',
223
- messageDelayMinMs: credentials.messageDelayMinMs || 5000,
224
- messageDelayMaxMs: credentials.messageDelayMaxMs || 9000,
225
- burstSize: (_a = credentials.burstSize) !== null && _a !== void 0 ? _a : 20,
226
- burstPauseMinMs: credentials.burstPauseMinMs || 30000,
227
- burstPauseMaxMs: credentials.burstPauseMaxMs || 60000,
228
- typingSimulation: credentials.typingSimulation !== false,
229
- dailySendLimit: (_b = credentials.dailySendLimit) !== null && _b !== void 0 ? _b : 500,
230
- checkRecipientExists: credentials.checkRecipientExists !== false,
231
- };
232
- }
233
- async function ensureConnected(cfg) {
234
- const antiBanCfg = {
235
- messageDelayMinMs: cfg.messageDelayMinMs,
236
- messageDelayMaxMs: cfg.messageDelayMaxMs,
237
- burstSize: cfg.burstSize,
238
- burstPauseMinMs: cfg.burstPauseMinMs,
239
- burstPauseMaxMs: cfg.burstPauseMaxMs,
240
- typingSimulation: cfg.typingSimulation,
241
- dailySendLimit: cfg.dailySendLimit,
242
- checkRecipientExists: cfg.checkRecipientExists,
45
+ // Find the server script
46
+ const serverPath = node_path_1.default.join(__dirname, 'WhatsAppServer.js');
47
+ if (!node_fs_1.default.existsSync(serverPath)) {
48
+ throw new Error(`WhatsApp server not found at ${serverPath}`);
49
+ }
50
+ // Start the server as background process
51
+ const env = {
52
+ ...process.env,
53
+ WHATSAPP_SESSION_PATH: cfg.sessionPath,
54
+ WHATSAPP_PORT: String(port),
55
+ MESSAGE_DELAY_MIN: String(cfg.messageDelayMinMs),
56
+ MESSAGE_DELAY_MAX: String(cfg.messageDelayMaxMs),
57
+ BURST_SIZE: String(cfg.burstSize),
58
+ BURST_PAUSE_MIN: String(cfg.burstPauseMinMs),
59
+ BURST_PAUSE_MAX: String(cfg.burstPauseMaxMs),
60
+ TYPING_SIMULATION: String(cfg.typingSimulation),
61
+ DAILY_SEND_LIMIT: String(cfg.dailySendLimit),
62
+ CHECK_RECIPIENT: String(cfg.checkRecipientExists),
243
63
  };
244
- socketConfig = antiBanCfg;
245
- sessionPath = cfg.sessionPath;
246
- // C1: Guard queue creation — only create once, preserve anti-ban state across calls
247
- if (!queue) {
248
- queue = new p_queue_1.default({ concurrency: 1 });
249
- }
250
- // Check if session exists on disk
251
- const resolvedPath = expandHome(cfg.sessionPath);
252
- const hasSession = node_fs_1.default.existsSync(resolvedPath) && node_fs_1.default.readdirSync(resolvedPath).length > 0;
253
- if (!hasSession) {
254
- throw new n8n_workflow_1.NodeApiError({}, {
255
- message: 'No WhatsApp session found. Run the WhatsApp Connect node first to scan QR code.',
256
- });
257
- }
258
- // Check if current socket is alive — reuse if possible
259
- if (socketInstance && socketStatus === 'connected') {
260
- try {
261
- // Test if socket is actually responding
262
- await socketInstance.query({ tag: 'iq', attrs: { id: 'ping', to: 's.whatsapp.net', type: 'get', xmlns: 'w:p' } });
263
- return socketInstance;
264
- }
265
- catch {
266
- // Socket is dead, create new one
267
- socketInstance = null;
268
- socketStatus = 'stopped';
269
- }
270
- }
271
- // Create fresh connection from saved session
272
- console.log('[WhatsApp] Creating fresh connection from saved session...');
273
- const sock = await initSocket(antiBanCfg, cfg.sessionPath);
274
- // Wait for connection to open (up to 20s)
275
- const connected = await new Promise((resolve) => {
276
- const check = setInterval(() => {
277
- if (socketStatus === 'connected') {
64
+ serverProcess = (0, node_child_process_1.spawn)('node', [serverPath], {
65
+ env,
66
+ stdio: 'ignore',
67
+ detached: true,
68
+ });
69
+ serverProcess.unref();
70
+ serverPort = port;
71
+ // Wait for server to start (up to 10s)
72
+ const started = await new Promise((resolve) => {
73
+ const start = Date.now();
74
+ const check = setInterval(async () => {
75
+ if (await isPortOpen(port)) {
278
76
  clearInterval(check);
279
77
  resolve(true);
280
78
  }
281
- if (socketStatus === 'logged_out' || socketStatus === 'error') {
79
+ if (Date.now() - start > 10000) {
282
80
  clearInterval(check);
283
81
  resolve(false);
284
82
  }
285
83
  }, 500);
286
- setTimeout(() => { clearInterval(check); resolve(false); }, 20000);
287
84
  });
288
- if (!connected) {
289
- throw new n8n_workflow_1.NodeApiError({}, {
290
- message: `WhatsApp connection failed (status: ${socketStatus}). Try running Connect node again.`,
291
- });
292
- }
293
- // Wait for credentials to be saved
294
- if (credsSavedPromise) {
295
- await credsSavedPromise;
296
- credsSavedPromise = null;
85
+ if (!started) {
86
+ throw new Error('WhatsApp server failed to start within 10 seconds');
297
87
  }
298
- return sock;
299
88
  }
300
- // Non-blocking version for Connect node — starts socket, returns QR URL if needed, doesn't wait
301
- async function connectOrGetQr(cfg) {
302
- if (socketStatus === 'connected' && socketInstance) {
303
- return { connected: true, message: 'Already connected' };
304
- }
305
- const antiBanCfg = {
306
- messageDelayMinMs: cfg.messageDelayMinMs,
307
- messageDelayMaxMs: cfg.messageDelayMaxMs,
308
- burstSize: cfg.burstSize,
309
- burstPauseMinMs: cfg.burstPauseMinMs,
310
- burstPauseMaxMs: cfg.burstPauseMaxMs,
311
- typingSimulation: cfg.typingSimulation,
312
- dailySendLimit: cfg.dailySendLimit,
313
- checkRecipientExists: cfg.checkRecipientExists,
314
- };
315
- socketConfig = antiBanCfg;
316
- sessionPath = cfg.sessionPath;
317
- if (!queue) {
318
- queue = new p_queue_1.default({ concurrency: 1 });
319
- }
320
- const resolvedPath = expandHome(cfg.sessionPath);
321
- const hasSession = node_fs_1.default.existsSync(resolvedPath) && node_fs_1.default.readdirSync(resolvedPath).length > 0;
322
- // If session exists, just connect (no QR needed)
323
- if (hasSession) {
324
- await initSocket(antiBanCfg, cfg.sessionPath);
325
- // Wait up to 15s for connection
326
- await new Promise((resolve) => {
327
- const check = setInterval(() => {
328
- if (socketStatus === 'connected' || socketStatus === 'error' || socketStatus === 'logged_out') {
329
- clearInterval(check);
330
- resolve();
331
- }
332
- }, 500);
333
- setTimeout(() => { clearInterval(check); resolve(); }, 15000);
334
- });
335
- // Wait for credentials to be fully saved to disk before returning
336
- if (credsSavedPromise) {
337
- await credsSavedPromise;
338
- credsSavedPromise = null;
339
- }
340
- if (socketStatus === 'connected') {
341
- return { connected: true, message: 'Connected successfully' };
342
- }
343
- return { connected: false, message: `Connection status: ${socketStatus}. Check n8n logs for details.` };
344
- }
345
- // No session — start socket and wait for QR (up to 30s)
346
- const sock = await initSocket(antiBanCfg, cfg.sessionPath);
347
- const qrData = await new Promise((resolve) => {
348
- qrResolve = resolve;
349
- setTimeout(() => {
350
- if (qrResolve) {
351
- qrResolve(null);
352
- qrResolve = null;
353
- }
354
- }, 30000);
355
- });
356
- if (!qrData) {
357
- return { connected: false, message: 'QR code not generated yet. Try again in a moment.' };
358
- }
359
- // Return QR URL immediately — user opens it in browser, scans, then runs Connect again
360
- return {
361
- connected: false,
362
- qrUrl: qrData.qrUrl,
363
- message: `Open this URL in your browser to scan QR:\n${qrData.qrUrl}\n\nAfter scanning, run this node again to complete connection.`,
364
- };
365
- }
366
- async function simulateTyping(jid, content, cfg) {
367
- if (!cfg.typingSimulation || !socketInstance)
368
- return;
369
- try {
370
- await socketInstance.sendPresenceUpdate('composing', jid);
371
- const len = (content.text || content.caption || '').length;
372
- await sleep(Math.min(randBetween(900, 1800) + len * 25, 4000));
373
- await socketInstance.sendPresenceUpdate('paused', jid);
374
- }
375
- catch {
376
- // presence is best-effort
377
- }
378
- }
379
- async function sendMessageWithAntiBan(to, content, cfg) {
380
- const sock = await ensureConnected(cfg);
381
- // Daily limit check
382
- if (cfg.dailySendLimit > 0) {
383
- const today = todayStartIso();
384
- if (sentTodayDate !== today) {
385
- sentTodayDate = today;
386
- sentTodayCount = 0;
387
- }
388
- if (sentTodayCount >= cfg.dailySendLimit) {
389
- throw new n8n_workflow_1.NodeApiError({}, {
390
- message: `Daily send limit (${cfg.dailySendLimit}) reached. Resets at midnight UTC.`,
391
- });
392
- }
393
- }
394
- const jid = normalizeRecipient(to);
395
- // Recipient validation (#6)
396
- if (cfg.checkRecipientExists && !jid.endsWith('@g.us')) {
397
- const results = await sock.onWhatsApp(jid);
398
- const result = results === null || results === void 0 ? void 0 : results[0];
399
- if (!(result === null || result === void 0 ? void 0 : result.exists)) {
400
- throw new n8n_workflow_1.NodeApiError({}, {
401
- message: `Recipient ${to} is not registered on WhatsApp.`,
402
- });
403
- }
404
- }
405
- // Queue the send through anti-ban
406
- const antiBanCfg = {
407
- messageDelayMinMs: cfg.messageDelayMinMs,
408
- messageDelayMaxMs: cfg.messageDelayMaxMs,
409
- burstSize: cfg.burstSize,
410
- burstPauseMinMs: cfg.burstPauseMinMs,
411
- burstPauseMaxMs: cfg.burstPauseMaxMs,
412
- typingSimulation: cfg.typingSimulation,
413
- dailySendLimit: cfg.dailySendLimit,
414
- checkRecipientExists: cfg.checkRecipientExists,
415
- };
416
- // #9: HTTP timeout race — if anti-ban delays push past 15s, return queued
417
- const task = queue.add(async () => {
418
- var _a;
419
- const wait = nextSendAt - Date.now();
420
- if (wait > 0)
421
- await sleep(wait);
422
- // Typing simulation (#4)
423
- await simulateTyping(jid, content, antiBanCfg);
424
- // Build message content
425
- const msgContent = {};
426
- if (content.text)
427
- msgContent.text = content.text;
428
- if (content.image)
429
- msgContent.image = content.image;
430
- if (content.document)
431
- msgContent.document = content.document;
432
- if (content.audio)
433
- msgContent.audio = content.audio;
434
- if (content.location)
435
- msgContent.location = content.location;
436
- if (content.mimetype)
437
- msgContent.mimetype = content.mimetype;
438
- if (content.fileName)
439
- msgContent.fileName = content.fileName;
440
- if (content.caption)
441
- msgContent.caption = content.caption;
442
- if (content.ptt !== undefined)
443
- msgContent.ptt = content.ptt;
444
- const response = await sock.sendMessage(jid, msgContent);
445
- // Update anti-ban timing (#2, #3)
446
- nextSendAt = Date.now() + sendGapMs(antiBanCfg) + burstPauseMs(antiBanCfg);
447
- // Update daily count (#5)
448
- if (cfg.dailySendLimit > 0)
449
- sentTodayCount++;
450
- if (!((_a = response === null || response === void 0 ? void 0 : response.key) === null || _a === void 0 ? void 0 : _a.id)) {
451
- throw new Error('WhatsApp did not return a message ID');
452
- }
453
- return {
454
- messageId: response.key.id,
455
- status: 'sent',
456
- recipient: jid,
457
- };
458
- });
459
- // #9: HTTP timeout race — hand off to background if anti-ban delay exceeds 15s
460
- let timer;
461
- const winner = await Promise.race([
462
- task.then(result => ({ result }), error => ({ error })),
463
- new Promise(resolve => { timer = setTimeout(() => resolve(null), 15000); }),
464
- ]);
465
- clearTimeout(timer);
466
- if (!winner) {
467
- // Queued behind anti-ban delays — let it send in background
468
- task.then(() => { }, () => { });
469
- return { messageId: 'queued', status: 'queued', recipient: jid };
470
- }
471
- const w = winner;
472
- if (w.error) {
473
- throw w.error;
474
- }
475
- return w.result;
476
- }
477
- function getConnectionStatus() {
478
- var _a;
479
- return {
480
- status: socketStatus,
481
- connected: socketStatus === 'connected',
482
- qrAvailable: socketStatus === 'qr_ready',
483
- queueSize: queue ? queue.size + queue.pending : 0,
484
- sentToday: sentTodayCount,
485
- dailyLimit: (_a = socketConfig === null || socketConfig === void 0 ? void 0 : socketConfig.dailySendLimit) !== null && _a !== void 0 ? _a : 0,
486
- lastError: lastDisconnectError,
487
- };
488
- }
489
- // Gracefully disconnect — important for n8n queue mode where multiple workers run
490
89
  async function disconnect() {
491
- if (socketInstance) {
90
+ // Signal server to stop (it's a separate process)
91
+ if (serverPort) {
492
92
  try {
493
- socketInstance.end(new Error('n8n execution finished'));
494
- }
495
- catch {
496
- // ignore
93
+ await fetch(`http://127.0.0.1:${serverPort}/health`);
497
94
  }
95
+ catch { }
498
96
  }
499
- socketInstance = null;
500
- socketStatus = 'stopped';
501
- ++generation; // Invalidate any pending handlers
502
97
  }
503
98
  function parseIncomingMessage(msg) {
504
99
  var _a, _b, _c, _d, _e, _f, _g, _h;
@@ -542,7 +137,6 @@ function parseIncomingMessage(msg) {
542
137
  return {
543
138
  messageId: msg.key.id,
544
139
  chatJid: msg.key.remoteJid,
545
- // C4: Simplified sender logic — participant is sender in groups, remoteJid in DMs
546
140
  sender: (_g = msg.key.participant) !== null && _g !== void 0 ? _g : msg.key.remoteJid,
547
141
  content: content || `[${messageType}]`,
548
142
  timestamp: new Date(timestampSeconds * 1000).toISOString(),
@@ -550,3 +144,17 @@ function parseIncomingMessage(msg) {
550
144
  messageType,
551
145
  };
552
146
  }
147
+ async function getWhatsAppCredentials(credentials) {
148
+ var _a, _b;
149
+ return {
150
+ sessionPath: credentials.sessionPath || '~/.n8n/whatsapp-auth',
151
+ messageDelayMinMs: credentials.messageDelayMinMs || 5000,
152
+ messageDelayMaxMs: credentials.messageDelayMaxMs || 9000,
153
+ burstSize: (_a = credentials.burstSize) !== null && _a !== void 0 ? _a : 20,
154
+ burstPauseMinMs: credentials.burstPauseMinMs || 30000,
155
+ burstPauseMaxMs: credentials.burstPauseMaxMs || 60000,
156
+ typingSimulation: credentials.typingSimulation !== false,
157
+ dailySendLimit: (_b = credentials.dailySendLimit) !== null && _b !== void 0 ? _b : 500,
158
+ checkRecipientExists: credentials.checkRecipientExists !== false,
159
+ };
160
+ }