@raevon/n8n-nodes-whatsapp 1.0.14 → 2.0.1

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,106 @@
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
+ const env = {
51
+ ...process.env,
52
+ WHATSAPP_SESSION_PATH: cfg.sessionPath,
53
+ WHATSAPP_PORT: String(port),
54
+ MESSAGE_DELAY_MIN: String(cfg.messageDelayMinMs),
55
+ MESSAGE_DELAY_MAX: String(cfg.messageDelayMaxMs),
56
+ BURST_SIZE: String(cfg.burstSize),
57
+ BURST_PAUSE_MIN: String(cfg.burstPauseMinMs),
58
+ BURST_PAUSE_MAX: String(cfg.burstPauseMaxMs),
59
+ TYPING_SIMULATION: String(cfg.typingSimulation),
60
+ DAILY_SEND_LIMIT: String(cfg.dailySendLimit),
61
+ CHECK_RECIPIENT: String(cfg.checkRecipientExists),
243
62
  };
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 });
63
+ // Use nohup + background for Docker compatibility
64
+ const cmd = `nohup node "${serverPath}" > /tmp/whatsapp-server-${port}.log 2>&1 &`;
65
+ try {
66
+ (0, node_child_process_1.execSync)(cmd, { env, stdio: 'ignore' });
249
67
  }
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
- });
68
+ catch (err) {
69
+ throw new Error(`Failed to start WhatsApp server: ${err.message}`);
257
70
  }
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') {
71
+ serverPort = port;
72
+ // Wait for server to start (up to 10s)
73
+ const started = await new Promise((resolve) => {
74
+ const start = Date.now();
75
+ const check = setInterval(async () => {
76
+ if (await isPortOpen(port)) {
278
77
  clearInterval(check);
279
78
  resolve(true);
280
79
  }
281
- if (socketStatus === 'logged_out' || socketStatus === 'error') {
80
+ if (Date.now() - start > 10000) {
282
81
  clearInterval(check);
283
82
  resolve(false);
284
83
  }
285
84
  }, 500);
286
- setTimeout(() => { clearInterval(check); resolve(false); }, 20000);
287
- });
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;
297
- }
298
- return sock;
299
- }
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
85
  });
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');
86
+ if (!started) {
87
+ // Read log for error details
88
+ let logContent = '';
89
+ try {
90
+ logContent = node_fs_1.default.readFileSync(`/tmp/whatsapp-server-${port}.log`, 'utf-8');
452
91
  }
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 };
92
+ catch { }
93
+ throw new Error(`WhatsApp server failed to start. Log: ${logContent.slice(0, 500)}`);
470
94
  }
471
- const w = winner;
472
- if (w.error) {
473
- throw w.error;
474
- }
475
- return w.result;
476
95
  }
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
96
  async function disconnect() {
491
- if (socketInstance) {
97
+ // Signal server to stop (it's a separate process)
98
+ if (serverPort) {
492
99
  try {
493
- socketInstance.end(new Error('n8n execution finished'));
494
- }
495
- catch {
496
- // ignore
100
+ await fetch(`http://127.0.0.1:${serverPort}/health`);
497
101
  }
102
+ catch { }
498
103
  }
499
- socketInstance = null;
500
- socketStatus = 'stopped';
501
- ++generation; // Invalidate any pending handlers
502
104
  }
503
105
  function parseIncomingMessage(msg) {
504
106
  var _a, _b, _c, _d, _e, _f, _g, _h;
@@ -542,7 +144,6 @@ function parseIncomingMessage(msg) {
542
144
  return {
543
145
  messageId: msg.key.id,
544
146
  chatJid: msg.key.remoteJid,
545
- // C4: Simplified sender logic — participant is sender in groups, remoteJid in DMs
546
147
  sender: (_g = msg.key.participant) !== null && _g !== void 0 ? _g : msg.key.remoteJid,
547
148
  content: content || `[${messageType}]`,
548
149
  timestamp: new Date(timestampSeconds * 1000).toISOString(),
@@ -550,3 +151,17 @@ function parseIncomingMessage(msg) {
550
151
  messageType,
551
152
  };
552
153
  }
154
+ async function getWhatsAppCredentials(credentials) {
155
+ var _a, _b;
156
+ return {
157
+ sessionPath: credentials.sessionPath || '~/.n8n/whatsapp-auth',
158
+ messageDelayMinMs: credentials.messageDelayMinMs || 5000,
159
+ messageDelayMaxMs: credentials.messageDelayMaxMs || 9000,
160
+ burstSize: (_a = credentials.burstSize) !== null && _a !== void 0 ? _a : 20,
161
+ burstPauseMinMs: credentials.burstPauseMinMs || 30000,
162
+ burstPauseMaxMs: credentials.burstPauseMaxMs || 60000,
163
+ typingSimulation: credentials.typingSimulation !== false,
164
+ dailySendLimit: (_b = credentials.dailySendLimit) !== null && _b !== void 0 ? _b : 500,
165
+ checkRecipientExists: credentials.checkRecipientExists !== false,
166
+ };
167
+ }