@raevon/n8n-nodes-whatsapp 2.0.14 → 3.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,3 +1,4 @@
1
+ import { type WASocket } from '@whiskeysockets/baileys';
1
2
  export interface WhatsAppCredentials {
2
3
  sessionPath: string;
3
4
  messageDelayMinMs: number;
@@ -9,9 +10,20 @@ export interface WhatsAppCredentials {
9
10
  dailySendLimit: number;
10
11
  checkRecipientExists: boolean;
11
12
  }
12
- export declare function getServerUrl(cfg: WhatsAppCredentials): string;
13
- export declare function ensureServerRunning(cfg: WhatsAppCredentials): Promise<void>;
14
- export declare function startServerIfNeeded(cfg: WhatsAppCredentials): Promise<void>;
15
- export declare function stopServer(cfg: WhatsAppCredentials): Promise<void>;
16
- export declare function disconnect(): Promise<void>;
17
13
  export declare function getWhatsAppCredentials(credentials: Record<string, any>): Promise<WhatsAppCredentials>;
14
+ export declare function ensureConnected(cfg: WhatsAppCredentials): Promise<WASocket>;
15
+ export declare function getConnectionStatus(): {
16
+ status: "stopped" | "connecting" | "qr_ready" | "connected" | "logged_out" | "error" | "disconnected";
17
+ connected: boolean;
18
+ qrAvailable: boolean;
19
+ sentToday: number;
20
+ dailyLimit: number;
21
+ lastError: string | null;
22
+ };
23
+ export declare function connectOrGetQr(cfg: WhatsAppCredentials): Promise<{
24
+ connected: boolean;
25
+ qrUrl?: string;
26
+ message: string;
27
+ }>;
28
+ export declare function parseIncomingMessage(msg: any): Record<string, any> | null;
29
+ export declare function sendMessageWithAntiBan(to: string, content: any, cfg: WhatsAppCredentials): Promise<any>;
@@ -36,368 +36,130 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.getServerUrl = getServerUrl;
40
- exports.ensureServerRunning = ensureServerRunning;
41
- exports.startServerIfNeeded = startServerIfNeeded;
42
- exports.stopServer = stopServer;
43
- exports.disconnect = disconnect;
44
39
  exports.getWhatsAppCredentials = getWhatsAppCredentials;
40
+ exports.ensureConnected = ensureConnected;
41
+ exports.getConnectionStatus = getConnectionStatus;
42
+ exports.connectOrGetQr = connectOrGetQr;
43
+ exports.parseIncomingMessage = parseIncomingMessage;
44
+ exports.sendMessageWithAntiBan = sendMessageWithAntiBan;
45
+ const n8n_workflow_1 = require("n8n-workflow");
46
+ const baileys_1 = __importStar(require("@whiskeysockets/baileys"));
45
47
  const node_path_1 = __importDefault(require("node:path"));
46
48
  const node_fs_1 = __importDefault(require("node:fs"));
47
- const node_net_1 = __importDefault(require("node:net"));
48
- const node_http_1 = __importDefault(require("node:http"));
49
- const node_url_1 = require("node:url");
50
- // Dynamic imports for Baileys (loaded only when server starts)
51
- let baileysModule = null;
49
+ const qrcode_1 = __importDefault(require("qrcode"));
50
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
51
+ const randBetween = (min, max) => max > min ? min + Math.floor(Math.random() * (max - min + 1)) : min;
52
52
  function expandHome(p) {
53
53
  if (!p.startsWith('~'))
54
54
  return p;
55
55
  return node_path_1.default.join(process.env.HOME || process.env.USERPROFILE || '', p.slice(1));
56
56
  }
57
- function getPort(cfg) {
58
- const hash = cfg.sessionPath.split('').reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0);
59
- return 3456 + (Math.abs(hash) % 1000);
57
+ const noop = () => { };
58
+ const silentLogger = { level: 'silent', info: noop, warn: noop, error: noop, debug: noop, fatal: noop, trace: noop, child: () => silentLogger };
59
+ // --- Singleton state ---
60
+ let socket = null;
61
+ let status = 'stopped';
62
+ let queue = null;
63
+ let nextSendAt = 0;
64
+ let sentInBurst = 0;
65
+ let sentTodayCount = 0;
66
+ let sentTodayDate = '';
67
+ let reconnectAttempts = 0;
68
+ let reconnectTimer = null;
69
+ let qrResolve = null;
70
+ let generation = 0;
71
+ let lastDisconnectError = null;
72
+ function todayStartIso() {
73
+ return new Date().toISOString().slice(0, 10) + 'T00:00:00.000Z';
60
74
  }
61
- function getServerUrl(cfg) {
62
- return `http://127.0.0.1:${getPort(cfg)}`;
75
+ function sendGapMs(cfg) {
76
+ return randBetween(cfg.messageDelayMinMs, cfg.messageDelayMaxMs);
63
77
  }
64
- async function isPortOpen(port) {
65
- return new Promise((resolve) => {
66
- const server = node_net_1.default.createServer();
67
- server.once('error', () => resolve(false));
68
- server.once('listening', () => { server.close(); resolve(true); });
69
- server.listen(port, '127.0.0.1');
70
- });
78
+ function burstPauseMs(cfg) {
79
+ if (!cfg.burstSize || ++sentInBurst < cfg.burstSize)
80
+ return 0;
81
+ sentInBurst = 0;
82
+ return randBetween(cfg.burstPauseMinMs, cfg.burstPauseMaxMs);
83
+ }
84
+ function normalizeRecipient(to) {
85
+ if (to.endsWith('@s.whatsapp.net') || to.endsWith('@g.us'))
86
+ return to;
87
+ const digits = to.replace(/\D/g, '');
88
+ if (!/^[1-9][0-9]{7,14}$/.test(digits))
89
+ throw new Error(`Invalid phone number: ${to}`);
90
+ return `${digits}@s.whatsapp.net`;
71
91
  }
72
- // --- In-memory server state (singleton per process) ---
73
- const servers = new Map();
74
- async function startServer(cfg) {
75
- const port = getPort(cfg);
76
- // Already running?
77
- if (servers.has(port))
92
+ async function scheduleReconnect(cfg) {
93
+ if (reconnectTimer)
78
94
  return;
79
- // Load Baileys dynamically
80
- if (!baileysModule) {
81
- baileysModule = await Promise.resolve().then(() => __importStar(require('@whiskeysockets/baileys')));
82
- }
83
- const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
84
- const randBetween = (min, max) => max > min ? min + Math.floor(Math.random() * (max - min + 1)) : min;
85
- const noop = () => { };
86
- const silentLogger = { level: 'silent', info: noop, warn: noop, error: noop, debug: noop, fatal: noop, trace: noop, child: () => silentLogger };
87
- const state = {
88
- server: null,
89
- status: 'stopped',
90
- qr: null,
91
- socket: null,
92
- messageBuffer: [],
93
- queue: null,
94
- nextSendAt: 0,
95
- sentInBurst: 0,
96
- sentTodayCount: 0,
97
- sentTodayDate: '',
98
- };
99
- async function connect() {
100
- if (state.socket && state.status === 'connected')
95
+ const base = Math.min(1000 * (2 ** reconnectAttempts), 60000);
96
+ const delay = Math.round(base * (0.8 + Math.random() * 0.4));
97
+ reconnectAttempts += 1;
98
+ reconnectTimer = setTimeout(() => { reconnectTimer = null; initSocket(cfg, sessionPathGlobal).catch(() => { }); }, delay);
99
+ }
100
+ let sessionPathGlobal = null;
101
+ async function initSocket(cfg, authPath) {
102
+ if (socket && status === 'connected')
103
+ return socket;
104
+ const gen = ++generation;
105
+ const resolvedPath = expandHome(authPath);
106
+ if (!node_fs_1.default.existsSync(resolvedPath))
107
+ node_fs_1.default.mkdirSync(resolvedPath, { recursive: true });
108
+ const { state, saveCreds } = await (0, baileys_1.useMultiFileAuthState)(resolvedPath);
109
+ const { version } = await (0, baileys_1.fetchLatestBaileysVersion)();
110
+ const sock = (0, baileys_1.default)({
111
+ version,
112
+ browser: baileys_1.Browsers.ubuntu('n8n WhatsApp Node'),
113
+ auth: { creds: state.creds, keys: (0, baileys_1.makeCacheableSignalKeyStore)(state.keys, silentLogger) },
114
+ markOnlineOnConnect: false,
115
+ syncFullHistory: false,
116
+ shouldSyncHistoryMessage: () => false,
117
+ generateHighQualityLinkPreview: false,
118
+ logger: silentLogger,
119
+ });
120
+ socket = sock;
121
+ status = 'connecting';
122
+ reconnectAttempts = 0;
123
+ sock.ev.on('creds.update', () => {
124
+ if (gen !== generation)
101
125
  return;
102
- const resolvedPath = expandHome(cfg.sessionPath);
103
- if (!node_fs_1.default.existsSync(resolvedPath))
104
- node_fs_1.default.mkdirSync(resolvedPath, { recursive: true });
105
- const { useMultiFileAuthState, fetchLatestBaileysVersion, makeWASocket, makeCacheableSignalKeyStore, Browsers, DisconnectReason } = baileysModule;
106
- const { state: authState, saveCreds } = await useMultiFileAuthState(resolvedPath);
107
- const { version } = await fetchLatestBaileysVersion();
108
- state.status = 'connecting';
109
- state.qr = null;
110
- state.socket = makeWASocket({
111
- version,
112
- browser: Browsers.ubuntu('n8n WhatsApp Node'),
113
- auth: { creds: authState.creds, keys: makeCacheableSignalKeyStore(authState.keys, silentLogger) },
114
- markOnlineOnConnect: false,
115
- syncFullHistory: false,
116
- shouldSyncHistoryMessage: () => false,
117
- generateHighQualityLinkPreview: false,
118
- logger: silentLogger,
119
- });
120
- state.socket.ev.on('creds.update', () => saveCreds().catch(() => { }));
121
- state.socket.ev.on('connection.update', (update) => {
122
- var _a, _b;
123
- const { connection, lastDisconnect, qr: newQr } = update;
124
- if (newQr) {
125
- state.qr = newQr;
126
- state.status = 'qr_ready';
127
- }
128
- if (connection === 'open') {
129
- state.status = 'connected';
130
- state.qr = null;
131
- return;
132
- }
133
- if (connection === 'close') {
134
- 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;
135
- if (code === DisconnectReason.loggedOut) {
136
- state.status = 'logged_out';
137
- state.socket = null;
138
- }
139
- else {
140
- state.status = 'disconnected';
141
- state.socket = null;
142
- setTimeout(() => connect().catch(() => { }), 5000);
143
- }
144
- }
145
- });
146
- state.socket.ev.on('messages.upsert', (upsert) => {
147
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
148
- const { messages, type } = upsert;
149
- if (type !== 'notify')
150
- return;
151
- for (const msg of messages) {
152
- if (msg.key.fromMe)
153
- continue;
154
- let content = null;
155
- const msgType = Object.keys(msg.message || {})[0] || 'unknown';
156
- if ((_a = msg.message) === null || _a === void 0 ? void 0 : _a.conversation)
157
- content = msg.message.conversation;
158
- else if ((_c = (_b = msg.message) === null || _b === void 0 ? void 0 : _b.extendedTextMessage) === null || _c === void 0 ? void 0 : _c.text)
159
- content = msg.message.extendedTextMessage.text;
160
- else if ((_e = (_d = msg.message) === null || _d === void 0 ? void 0 : _d.imageMessage) === null || _e === void 0 ? void 0 : _e.caption)
161
- content = msg.message.imageMessage.caption;
162
- else if ((_g = (_f = msg.message) === null || _f === void 0 ? void 0 : _f.videoMessage) === null || _g === void 0 ? void 0 : _g.caption)
163
- content = msg.message.videoMessage.caption;
164
- else if ((_j = (_h = msg.message) === null || _h === void 0 ? void 0 : _h.documentMessage) === null || _j === void 0 ? void 0 : _j.fileName)
165
- content = msg.message.documentMessage.fileName;
166
- else if ((_k = msg.message) === null || _k === void 0 ? void 0 : _k.audioMessage)
167
- content = '[Audio]';
168
- else
169
- content = `[${msgType}]`;
170
- const ts = msg.messageTimestamp != null ? Number(msg.messageTimestamp) : Date.now() / 1000;
171
- state.messageBuffer.push({ messageId: msg.key.id, chatJid: msg.key.remoteJid, sender: (_l = msg.key.participant) !== null && _l !== void 0 ? _l : msg.key.remoteJid, content, timestamp: new Date(ts * 1000).toISOString(), isFromMe: false, messageType: msgType });
172
- }
173
- });
174
- if (!state.queue) {
175
- const PQueue = (await Promise.resolve().then(() => __importStar(require('p-queue')))).default;
176
- state.queue = new PQueue({ concurrency: 1 });
177
- }
178
- }
179
- function normalizeRecipient(to) {
180
- if (to.endsWith('@s.whatsapp.net') || to.endsWith('@g.us'))
181
- return to;
182
- const digits = to.replace(/\D/g, '');
183
- if (!/^[1-9][0-9]{7,14}$/.test(digits))
184
- throw new Error(`Invalid phone number: ${to}`);
185
- return `${digits}@s.whatsapp.net`;
186
- }
187
- async function sendMessage(to, content) {
188
- var _a;
189
- if (!state.socket || state.status !== 'connected')
190
- throw new Error('WhatsApp not connected');
191
- if (cfg.dailySendLimit > 0) {
192
- const today = new Date().toISOString().slice(0, 10) + 'T00:00:00.000Z';
193
- if (state.sentTodayDate !== today) {
194
- state.sentTodayDate = today;
195
- state.sentTodayCount = 0;
196
- }
197
- if (state.sentTodayCount >= cfg.dailySendLimit)
198
- throw new Error(`Daily limit (${cfg.dailySendLimit}) reached`);
199
- }
200
- const jid = normalizeRecipient(to);
201
- if (cfg.checkRecipientExists && !jid.endsWith('@g.us')) {
202
- const results = await state.socket.onWhatsApp(jid);
203
- if (!((_a = results === null || results === void 0 ? void 0 : results[0]) === null || _a === void 0 ? void 0 : _a.exists))
204
- throw new Error(`Recipient ${to} not on WhatsApp`);
205
- }
206
- const task = state.queue.add(async () => {
207
- var _a;
208
- const wait = state.nextSendAt - Date.now();
209
- if (wait > 0)
210
- await sleep(wait);
211
- if (cfg.typingSimulation) {
212
- try {
213
- await state.socket.sendPresenceUpdate('composing', jid);
214
- const len = (content.text || content.caption || '').length;
215
- await sleep(Math.min(randBetween(900, 1800) + len * 25, 4000));
216
- await state.socket.sendPresenceUpdate('paused', jid);
217
- }
218
- catch { }
219
- }
220
- const response = await state.socket.sendMessage(jid, content);
221
- const gap = randBetween(cfg.messageDelayMinMs, cfg.messageDelayMaxMs);
222
- let burst = 0;
223
- if (cfg.burstSize && ++state.sentInBurst >= cfg.burstSize) {
224
- state.sentInBurst = 0;
225
- burst = randBetween(cfg.burstPauseMinMs, cfg.burstPauseMaxMs);
226
- }
227
- state.nextSendAt = Date.now() + gap + burst;
228
- if (cfg.dailySendLimit > 0)
229
- state.sentTodayCount++;
230
- if (!((_a = response === null || response === void 0 ? void 0 : response.key) === null || _a === void 0 ? void 0 : _a.id))
231
- throw new Error('No message ID returned');
232
- return { messageId: response.key.id, status: 'sent', recipient: jid };
233
- });
234
- let timer;
235
- const winner = await Promise.race([
236
- task.then((r) => ({ result: r }), (e) => ({ error: e })),
237
- new Promise(r => { timer = setTimeout(() => r(null), 15000); }),
238
- ]);
239
- clearTimeout(timer);
240
- if (!winner) {
241
- task.then(() => { }, () => { });
242
- return { messageId: 'queued', status: 'queued', recipient: jid };
126
+ saveCreds().catch(() => { });
127
+ });
128
+ sock.ev.on('connection.update', async (update) => {
129
+ var _a, _b;
130
+ if (gen !== generation)
131
+ return;
132
+ const { connection, lastDisconnect, qr: newQr } = update;
133
+ if (newQr) {
134
+ qrResolve && qrResolve({ qr: newQr, qrUrl: '' });
135
+ qrResolve = null;
243
136
  }
244
- if (winner.error)
245
- throw winner.error;
246
- return winner.result;
247
- }
248
- // HTTP server
249
- const httpServer = node_http_1.default.createServer(async (req, res) => {
250
- const url = new node_url_1.URL(req.url || '/', `http://localhost:${port}`);
251
- res.setHeader('Access-Control-Allow-Origin', '*');
252
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
253
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
254
- if (req.method === 'OPTIONS') {
255
- res.writeHead(204);
256
- res.end();
137
+ if (connection === 'open') {
138
+ status = 'connected';
139
+ reconnectAttempts = 0;
257
140
  return;
258
141
  }
259
- try {
260
- if (url.pathname === '/health') {
261
- res.writeHead(200, { 'Content-Type': 'application/json' });
262
- res.end(JSON.stringify({ ok: true, status: state.status, connected: state.status === 'connected' }));
263
- }
264
- else if (url.pathname === '/status') {
265
- res.writeHead(200, { 'Content-Type': 'application/json' });
266
- res.end(JSON.stringify({ status: state.status, connected: state.status === 'connected', qrAvailable: state.status === 'qr_ready', sentToday: state.sentTodayCount, dailyLimit: cfg.dailySendLimit }));
267
- }
268
- else if (url.pathname === '/connect') {
269
- await connect();
270
- const result = await new Promise(r => {
271
- const check = setInterval(() => {
272
- if (state.status === 'connected') {
273
- clearInterval(check);
274
- r({ connected: true, message: 'Connected' });
275
- }
276
- if (state.qr) {
277
- clearInterval(check);
278
- Promise.resolve().then(() => __importStar(require('qrcode'))).then(QRCode => QRCode.toDataURL(state.qr, { width: 300, margin: 2 }).then(qrUrl => r({ connected: false, qrUrl, message: 'Scan QR' })));
279
- }
280
- }, 500);
281
- setTimeout(() => { clearInterval(check); r({ connected: false, status: state.status, message: 'Timeout' }); }, 30000);
282
- });
283
- res.writeHead(200, { 'Content-Type': 'application/json' });
284
- res.end(JSON.stringify(result));
285
- }
286
- else if (url.pathname === '/qr' && state.qr) {
287
- const QRCode = await Promise.resolve().then(() => __importStar(require('qrcode')));
288
- const qrUrl = await QRCode.toDataURL(state.qr, { width: 400, margin: 2 });
289
- res.writeHead(200, { 'Content-Type': 'text/html' });
290
- res.end(`<!DOCTYPE html><html><body style="display:flex;justify-content:center;align-items:center;height:100vh;background:#111"><img src="${qrUrl}"/></body></html>`);
291
- }
292
- else if (url.pathname === '/send' && req.method === 'POST') {
293
- let body = '';
294
- req.on('data', chunk => body += chunk);
295
- req.on('end', async () => {
296
- try {
297
- const { to, ...content } = JSON.parse(body);
298
- if (!to)
299
- throw new Error('Missing "to"');
300
- const result = await sendMessage(to, content);
301
- res.writeHead(200, { 'Content-Type': 'application/json' });
302
- res.end(JSON.stringify(result));
303
- }
304
- catch (err) {
305
- res.writeHead(400, { 'Content-Type': 'application/json' });
306
- res.end(JSON.stringify({ error: err.message }));
307
- }
308
- });
309
- }
310
- else if (url.pathname === '/messages') {
311
- const msgs = state.messageBuffer.splice(0, state.messageBuffer.length);
312
- res.writeHead(200, { 'Content-Type': 'application/json' });
313
- res.end(JSON.stringify({ messages: msgs, count: msgs.length }));
314
- }
315
- else if (url.pathname === '/signout') {
316
- if (state.socket) {
317
- try {
318
- state.socket.end(new Error('Sign out'));
319
- }
320
- catch { }
321
- state.socket = null;
322
- }
323
- state.status = 'stopped';
324
- const p = expandHome(cfg.sessionPath);
325
- if (node_fs_1.default.existsSync(p)) {
326
- node_fs_1.default.readdirSync(p).forEach(f => node_fs_1.default.unlinkSync(node_path_1.default.join(p, f)));
327
- node_fs_1.default.rmdirSync(p);
328
- }
329
- res.writeHead(200, { 'Content-Type': 'application/json' });
330
- res.end(JSON.stringify({ success: true, message: 'Signed out' }));
142
+ if (connection === 'close') {
143
+ 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;
144
+ lastDisconnectError = `Code: ${code}`;
145
+ if (code === baileys_1.DisconnectReason.loggedOut) {
146
+ status = 'logged_out';
147
+ socket = null;
148
+ ++generation;
331
149
  }
332
150
  else {
333
- res.writeHead(404);
334
- res.end('Not found');
151
+ status = 'disconnected';
152
+ socket = null;
153
+ scheduleReconnect(cfg);
335
154
  }
336
155
  }
337
- catch (err) {
338
- res.writeHead(500, { 'Content-Type': 'application/json' });
339
- res.end(JSON.stringify({ error: err.message }));
340
- }
341
156
  });
342
- await new Promise(r => httpServer.listen(port, () => r()));
343
- state.server = httpServer;
344
- servers.set(port, state);
345
- console.log(`[WhatsApp] Server started on port ${port}`);
346
- }
347
- async function ensureServerRunning(cfg) {
348
- const port = getPort(cfg);
349
- // Just check if server is running — don't start it
350
- try {
351
- const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(3000) });
352
- if (res.ok)
353
- return; // Server alive
157
+ if (!queue) {
158
+ const PQ = (await Promise.resolve().then(() => __importStar(require('p-queue')))).default;
159
+ queue = new PQ({ concurrency: 1 });
354
160
  }
355
- catch { }
356
- throw new Error(`WhatsApp server not running on port ${port}. Run "Start Server" or "Connect" first.`);
161
+ return sock;
357
162
  }
358
- async function startServerIfNeeded(cfg) {
359
- const port = getPort(cfg);
360
- // Check if already running
361
- try {
362
- const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(3000) });
363
- if (res.ok)
364
- return; // Already running
365
- }
366
- catch { }
367
- // Port free — start server
368
- if (await isPortOpen(port)) {
369
- await startServer(cfg);
370
- return;
371
- }
372
- // Port occupied by something else
373
- throw new Error(`Port ${port} occupied. Restart n8n container, then try again.`);
374
- }
375
- async function stopServer(cfg) {
376
- const port = getPort(cfg);
377
- // Try graceful shutdown via the server's own /signout endpoint
378
- try {
379
- await fetch(`http://127.0.0.1:${port}/signout`, { method: 'POST', signal: AbortSignal.timeout(3000) });
380
- }
381
- catch { }
382
- // Clean up in-memory state
383
- const state = servers.get(port);
384
- if (state) {
385
- if (state.socket) {
386
- try {
387
- state.socket.end(new Error('Server stop'));
388
- }
389
- catch { }
390
- }
391
- if (state.server) {
392
- try {
393
- state.server.close();
394
- }
395
- catch { }
396
- }
397
- servers.delete(port);
398
- }
399
- }
400
- async function disconnect() { }
401
163
  async function getWhatsAppCredentials(credentials) {
402
164
  var _a, _b;
403
165
  return {
@@ -412,3 +174,164 @@ async function getWhatsAppCredentials(credentials) {
412
174
  checkRecipientExists: credentials.checkRecipientExists !== false,
413
175
  };
414
176
  }
177
+ async function ensureConnected(cfg) {
178
+ sessionPathGlobal = cfg.sessionPath;
179
+ const resolvedPath = expandHome(cfg.sessionPath);
180
+ const hasSession = node_fs_1.default.existsSync(resolvedPath) && node_fs_1.default.readdirSync(resolvedPath).length > 0;
181
+ if (!hasSession) {
182
+ // No session — connect and wait for QR
183
+ const sock = await initSocket(cfg, cfg.sessionPath);
184
+ const qrData = await new Promise((resolve) => {
185
+ qrResolve = resolve;
186
+ setTimeout(() => { if (qrResolve) {
187
+ qrResolve(null);
188
+ qrResolve = null;
189
+ } }, 90000);
190
+ });
191
+ if (!qrData)
192
+ throw new n8n_workflow_1.NodeApiError({}, { message: 'QR expired. Run Connect again.' });
193
+ // Wait for connection
194
+ await new Promise((resolve, reject) => {
195
+ const timeout = setTimeout(() => reject(new Error('Connection timeout')), 30000);
196
+ const check = setInterval(() => {
197
+ if (status === 'connected') {
198
+ clearTimeout(timeout);
199
+ clearInterval(check);
200
+ resolve();
201
+ }
202
+ if (status === 'logged_out' || status === 'error') {
203
+ clearTimeout(timeout);
204
+ clearInterval(check);
205
+ reject(new Error('Connection failed'));
206
+ }
207
+ }, 1000);
208
+ });
209
+ return sock;
210
+ }
211
+ // Session exists — connect
212
+ const sock = await initSocket(cfg, cfg.sessionPath);
213
+ await new Promise((resolve) => {
214
+ const check = setInterval(() => {
215
+ if (status === 'connected' || status === 'error' || status === 'logged_out') {
216
+ clearInterval(check);
217
+ resolve();
218
+ }
219
+ }, 500);
220
+ setTimeout(() => { clearInterval(check); resolve(); }, 20000);
221
+ });
222
+ if (status !== 'connected')
223
+ throw new n8n_workflow_1.NodeApiError({}, { message: `Connection failed (status: ${status})` });
224
+ return sock;
225
+ }
226
+ function getConnectionStatus() {
227
+ return { status, connected: status === 'connected', qrAvailable: status === 'qr_ready', sentToday: sentTodayCount, dailyLimit: 500, lastError: lastDisconnectError };
228
+ }
229
+ async function connectOrGetQr(cfg) {
230
+ sessionPathGlobal = cfg.sessionPath;
231
+ const resolvedPath = expandHome(cfg.sessionPath);
232
+ const hasSession = node_fs_1.default.existsSync(resolvedPath) && node_fs_1.default.readdirSync(resolvedPath).length > 0;
233
+ if (hasSession) {
234
+ await initSocket(cfg, cfg.sessionPath);
235
+ await new Promise((resolve) => {
236
+ const check = setInterval(() => { if (status === 'connected' || status === 'error' || status === 'logged_out') {
237
+ clearInterval(check);
238
+ resolve();
239
+ } }, 500);
240
+ setTimeout(() => { clearInterval(check); resolve(); }, 15000);
241
+ });
242
+ if (status === 'connected')
243
+ return { connected: true, message: 'Connected successfully' };
244
+ return { connected: false, message: `Status: ${status}` };
245
+ }
246
+ const sock = await initSocket(cfg, cfg.sessionPath);
247
+ const qrData = await new Promise((resolve) => {
248
+ qrResolve = resolve;
249
+ setTimeout(() => { if (qrResolve) {
250
+ qrResolve(null);
251
+ qrResolve = null;
252
+ } }, 30000);
253
+ });
254
+ if (!qrData)
255
+ return { connected: false, message: 'QR not generated. Try again.' };
256
+ const qrDataUrl = await qrcode_1.default.toDataURL(qrData.qr, { width: 300, margin: 2 });
257
+ return { connected: false, qrUrl: qrDataUrl, message: `Open this URL to scan QR:\n${qrDataUrl}\n\nAfter scanning, run Connect again.` };
258
+ }
259
+ function parseIncomingMessage(msg) {
260
+ var _a, _b, _c, _d, _e, _f, _g, _h;
261
+ if (!msg.message || !msg.key || !msg.key.remoteJid)
262
+ return null;
263
+ let content = null;
264
+ const messageType = Object.keys(msg.message)[0];
265
+ if (msg.message.conversation)
266
+ content = msg.message.conversation;
267
+ else if ((_a = msg.message.extendedTextMessage) === null || _a === void 0 ? void 0 : _a.text)
268
+ content = msg.message.extendedTextMessage.text;
269
+ else if ((_b = msg.message.imageMessage) === null || _b === void 0 ? void 0 : _b.caption)
270
+ content = msg.message.imageMessage.caption;
271
+ else if ((_c = msg.message.videoMessage) === null || _c === void 0 ? void 0 : _c.caption)
272
+ content = msg.message.videoMessage.caption;
273
+ else if ((_d = msg.message.documentMessage) === null || _d === void 0 ? void 0 : _d.fileName)
274
+ content = msg.message.documentMessage.fileName;
275
+ else if (msg.message.audioMessage)
276
+ content = '[Audio]';
277
+ else if (msg.message.stickerMessage)
278
+ content = '[Sticker]';
279
+ else if (msg.message.locationMessage)
280
+ content = `[Location] ${msg.message.locationMessage.address || ''}`;
281
+ else if ((_e = msg.message.contactMessage) === null || _e === void 0 ? void 0 : _e.displayName)
282
+ content = `[Contact] ${msg.message.contactMessage.displayName}`;
283
+ else if ((_f = msg.message.pollCreationMessage) === null || _f === void 0 ? void 0 : _f.name)
284
+ content = `[Poll] ${msg.message.pollCreationMessage.name}`;
285
+ const ts = msg.messageTimestamp != null ? Number(msg.messageTimestamp) : Date.now() / 1000;
286
+ return { messageId: msg.key.id, chatJid: msg.key.remoteJid, sender: (_g = msg.key.participant) !== null && _g !== void 0 ? _g : msg.key.remoteJid, content: content || `[${messageType}]`, timestamp: new Date(ts * 1000).toISOString(), isFromMe: (_h = msg.key.fromMe) !== null && _h !== void 0 ? _h : false, messageType };
287
+ }
288
+ async function sendMessageWithAntiBan(to, content, cfg) {
289
+ var _a;
290
+ const sock = await ensureConnected(cfg);
291
+ if (cfg.dailySendLimit > 0) {
292
+ const today = todayStartIso();
293
+ if (sentTodayDate !== today) {
294
+ sentTodayDate = today;
295
+ sentTodayCount = 0;
296
+ }
297
+ if (sentTodayCount >= cfg.dailySendLimit)
298
+ throw new Error(`Daily limit reached`);
299
+ }
300
+ const jid = normalizeRecipient(to);
301
+ if (cfg.checkRecipientExists && !jid.endsWith('@g.us')) {
302
+ const results = await sock.onWhatsApp(jid);
303
+ if (!((_a = results === null || results === void 0 ? void 0 : results[0]) === null || _a === void 0 ? void 0 : _a.exists))
304
+ throw new Error(`Recipient not on WhatsApp`);
305
+ }
306
+ const task = queue.add(async () => {
307
+ var _a;
308
+ const wait = nextSendAt - Date.now();
309
+ if (wait > 0)
310
+ await sleep(wait);
311
+ if (cfg.typingSimulation) {
312
+ try {
313
+ await sock.sendPresenceUpdate('composing', jid);
314
+ await sleep(Math.min(randBetween(900, 1800) + (content.text || '').length * 25, 4000));
315
+ await sock.sendPresenceUpdate('paused', jid);
316
+ }
317
+ catch { }
318
+ }
319
+ const response = await sock.sendMessage(jid, content);
320
+ nextSendAt = Date.now() + sendGapMs(cfg) + burstPauseMs(cfg);
321
+ if (cfg.dailySendLimit > 0)
322
+ sentTodayCount++;
323
+ if (!((_a = response === null || response === void 0 ? void 0 : response.key) === null || _a === void 0 ? void 0 : _a.id))
324
+ throw new Error('No message ID');
325
+ return { messageId: response.key.id, status: 'sent', recipient: jid };
326
+ });
327
+ let timer;
328
+ const winner = await Promise.race([task.then((r) => ({ result: r }), (e) => ({ error: e })), new Promise(r => { timer = setTimeout(() => r(null), 15000); })]);
329
+ clearTimeout(timer);
330
+ if (!winner) {
331
+ task.then(() => { }, () => { });
332
+ return { messageId: 'queued', status: 'queued', recipient: jid };
333
+ }
334
+ if (winner.error)
335
+ throw winner.error;
336
+ return winner.result;
337
+ }
@@ -1,18 +1,8 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.WhatsAppConnect = void 0;
7
4
  const n8n_workflow_1 = require("n8n-workflow");
8
5
  const WhatsAppApiHelper_1 = require("./WhatsAppApiHelper");
9
- const node_fs_1 = __importDefault(require("node:fs"));
10
- const node_path_1 = __importDefault(require("node:path"));
11
- function expandHome(p) {
12
- if (!p.startsWith('~'))
13
- return p;
14
- return node_path_1.default.join(process.env.HOME || process.env.USERPROFILE || '', p.slice(1));
15
- }
16
6
  class WhatsAppConnect {
17
7
  constructor() {
18
8
  this.description = {
@@ -22,23 +12,17 @@ class WhatsAppConnect {
22
12
  group: ['transform'],
23
13
  version: 1,
24
14
  subtitle: '={{$parameter["operation"]}}',
25
- description: 'Connect to WhatsApp — manage server, scan QR, sign out',
15
+ description: 'Connect to WhatsApp — scan QR on first run',
26
16
  defaults: { name: 'WhatsApp Connect' },
27
17
  inputs: ['main'],
28
18
  outputs: ['main'],
29
19
  credentials: [{ name: 'whatsappApi', required: true }],
30
20
  properties: [
31
21
  {
32
- displayName: 'Operation',
33
- name: 'operation',
34
- type: 'options',
35
- noDataExpression: true,
22
+ displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true,
36
23
  options: [
37
- { name: 'Connect', value: 'connect', description: 'Start server and connect to WhatsApp', action: 'Connect to WhatsApp' },
38
- { name: 'Get Status', value: 'status', description: 'Get server and connection status', action: 'Get status' },
39
- { name: 'Start Server', value: 'startServer', description: 'Start the WhatsApp background server', action: 'Start server' },
40
- { name: 'Stop Server', value: 'stopServer', description: 'Stop the WhatsApp background server', action: 'Stop server' },
41
- { name: 'Sign Out', value: 'signOut', description: 'Stop server and delete session', action: 'Sign out' },
24
+ { name: 'Connect', value: 'connect', description: 'Connect to WhatsApp', action: 'Connect' },
25
+ { name: 'Get Status', value: 'status', description: 'Get connection status', action: 'Get status' },
42
26
  ],
43
27
  default: 'connect',
44
28
  },
@@ -53,78 +37,13 @@ class WhatsAppConnect {
53
37
  const operation = this.getNodeParameter('operation', 0);
54
38
  for (let i = 0; i < items.length; i++) {
55
39
  try {
56
- if (operation === 'startServer') {
57
- await (0, WhatsAppApiHelper_1.startServerIfNeeded)(cfg);
58
- returnData.push({
59
- json: { success: true, message: 'Server started', port: (0, WhatsAppApiHelper_1.getServerUrl)(cfg), sessionPath: cfg.sessionPath },
60
- pairedItem: { item: i },
61
- });
62
- }
63
- else if (operation === 'stopServer') {
64
- // Try graceful shutdown via /signout
65
- try {
66
- await fetch(`${(0, WhatsAppApiHelper_1.getServerUrl)(cfg)}/signout`, { method: 'POST', signal: AbortSignal.timeout(3000) });
67
- returnData.push({
68
- json: { success: true, message: 'Server stopped', sessionPath: cfg.sessionPath },
69
- pairedItem: { item: i },
70
- });
71
- }
72
- catch {
73
- returnData.push({
74
- json: { success: false, message: 'Server may be in another worker. Restart n8n container to fully stop.', sessionPath: cfg.sessionPath },
75
- pairedItem: { item: i },
76
- });
77
- }
78
- }
79
- else if (operation === 'connect') {
80
- try {
81
- await (0, WhatsAppApiHelper_1.startServerIfNeeded)(cfg);
82
- }
83
- catch (err) {
84
- returnData.push({
85
- json: { connected: false, error: err.message, sessionPath: cfg.sessionPath },
86
- pairedItem: { item: i },
87
- });
88
- continue;
89
- }
90
- try {
91
- const serverUrl = (0, WhatsAppApiHelper_1.getServerUrl)(cfg);
92
- const response = await fetch(`${serverUrl}/connect`, { method: 'POST', signal: AbortSignal.timeout(60000) });
93
- const result = await response.json();
94
- returnData.push({ json: { ...result, sessionPath: cfg.sessionPath }, pairedItem: { item: i } });
95
- }
96
- catch (err) {
97
- returnData.push({
98
- json: { connected: false, error: `Connect failed: ${err.message}`, sessionPath: cfg.sessionPath },
99
- pairedItem: { item: i },
100
- });
101
- }
40
+ if (operation === 'connect') {
41
+ const result = await (0, WhatsAppApiHelper_1.connectOrGetQr)(cfg);
42
+ returnData.push({ json: { ...result, sessionPath: cfg.sessionPath }, pairedItem: { item: i } });
102
43
  }
103
44
  else if (operation === 'status') {
104
- try {
105
- await (0, WhatsAppApiHelper_1.ensureServerRunning)(cfg);
106
- const response = await fetch(`${(0, WhatsAppApiHelper_1.getServerUrl)(cfg)}/status`);
107
- const result = await response.json();
108
- returnData.push({ json: { ...result, sessionPath: cfg.sessionPath }, pairedItem: { item: i } });
109
- }
110
- catch {
111
- returnData.push({
112
- json: { status: 'server_not_running', connected: false, sessionPath: cfg.sessionPath },
113
- pairedItem: { item: i },
114
- });
115
- }
116
- }
117
- else if (operation === 'signOut') {
118
- await (0, WhatsAppApiHelper_1.stopServer)(cfg);
119
- const resolvedPath = expandHome(cfg.sessionPath);
120
- if (node_fs_1.default.existsSync(resolvedPath)) {
121
- node_fs_1.default.readdirSync(resolvedPath).forEach(f => node_fs_1.default.unlinkSync(node_path_1.default.join(resolvedPath, f)));
122
- node_fs_1.default.rmdirSync(resolvedPath);
123
- }
124
- returnData.push({
125
- json: { success: true, message: 'Signed out. Session deleted.', sessionPath: cfg.sessionPath },
126
- pairedItem: { item: i },
127
- });
45
+ const status = (0, WhatsAppApiHelper_1.getConnectionStatus)();
46
+ returnData.push({ json: { ...status, sessionPath: cfg.sessionPath }, pairedItem: { item: i } });
128
47
  }
129
48
  }
130
49
  catch (error) {
@@ -12,95 +12,31 @@ class WhatsAppSend {
12
12
  group: ['transform'],
13
13
  version: 1,
14
14
  subtitle: '={{$parameter["operation"]}}',
15
- description: 'Send WhatsApp messages via background server',
15
+ description: 'Send WhatsApp messages with anti-ban protection',
16
16
  defaults: { name: 'WhatsApp Send' },
17
17
  inputs: ['main'],
18
18
  outputs: ['main'],
19
19
  credentials: [{ name: 'whatsappApi', required: true }],
20
20
  properties: [
21
21
  {
22
- displayName: 'Operation',
23
- name: 'operation',
24
- type: 'options',
25
- noDataExpression: true,
22
+ displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true,
26
23
  options: [
27
- { name: 'Send Text', value: 'sendText', description: 'Send a text message', action: 'Send text message' },
28
- { name: 'Send Image', value: 'sendImage', description: 'Send an image with optional caption', action: 'Send image message' },
29
- { name: 'Send Document', value: 'sendDocument', description: 'Send a document/file', action: 'Send document' },
30
- { name: 'Send Audio', value: 'sendAudio', description: 'Send an audio message or voice note', action: 'Send audio message' },
31
- { name: 'Send Location', value: 'sendLocation', description: 'Send a location pin', action: 'Send location' },
24
+ { name: 'Send Text', value: 'sendText', description: 'Send a text message', action: 'Send text' },
25
+ { name: 'Send Image', value: 'sendImage', description: 'Send an image', action: 'Send image' },
26
+ { name: 'Send Document', value: 'sendDocument', description: 'Send a document', action: 'Send document' },
27
+ { name: 'Send Audio', value: 'sendAudio', description: 'Send audio', action: 'Send audio' },
28
+ { name: 'Send Location', value: 'sendLocation', description: 'Send location', action: 'Send location' },
32
29
  ],
33
30
  default: 'sendText',
34
31
  },
35
- {
36
- displayName: 'Recipient',
37
- name: 'recipient',
38
- type: 'string',
39
- default: '',
40
- required: true,
41
- description: 'Phone number (e.g., +1234567890) or WhatsApp JID',
42
- },
43
- {
44
- displayName: 'Text',
45
- name: 'text',
46
- type: 'string',
47
- default: '',
48
- required: true,
49
- displayOptions: { show: { operation: ['sendText'] } },
50
- description: 'Message text to send',
51
- },
52
- {
53
- displayName: 'Binary Property',
54
- name: 'binaryProperty',
55
- type: 'string',
56
- default: 'data',
57
- required: true,
58
- displayOptions: { show: { operation: ['sendImage', 'sendDocument', 'sendAudio'] } },
59
- description: 'Name of the binary property containing the file data',
60
- },
61
- {
62
- displayName: 'Caption',
63
- name: 'caption',
64
- type: 'string',
65
- default: '',
66
- displayOptions: { show: { operation: ['sendImage', 'sendDocument'] } },
67
- description: 'Optional caption',
68
- },
69
- {
70
- displayName: 'File Name',
71
- name: 'fileName',
72
- type: 'string',
73
- default: 'file',
74
- required: true,
75
- displayOptions: { show: { operation: ['sendDocument'] } },
76
- description: 'Name of the file being sent',
77
- },
78
- {
79
- displayName: 'Voice Note',
80
- name: 'ptt',
81
- type: 'boolean',
82
- default: false,
83
- displayOptions: { show: { operation: ['sendAudio'] } },
84
- description: 'Send as voice note',
85
- },
86
- {
87
- displayName: 'Latitude',
88
- name: 'latitude',
89
- type: 'number',
90
- default: 0,
91
- required: true,
92
- displayOptions: { show: { operation: ['sendLocation'] } },
93
- description: 'Location latitude',
94
- },
95
- {
96
- displayName: 'Longitude',
97
- name: 'longitude',
98
- type: 'number',
99
- default: 0,
100
- required: true,
101
- displayOptions: { show: { operation: ['sendLocation'] } },
102
- description: 'Location longitude',
103
- },
32
+ { displayName: 'Recipient', name: 'recipient', type: 'string', default: '', required: true, description: 'Phone number or JID' },
33
+ { displayName: 'Text', name: 'text', type: 'string', default: '', required: true, displayOptions: { show: { operation: ['sendText'] } }, description: 'Message text' },
34
+ { displayName: 'Binary Property', name: 'binaryProperty', type: 'string', default: 'data', required: true, displayOptions: { show: { operation: ['sendImage', 'sendDocument', 'sendAudio'] } }, description: 'Binary property name' },
35
+ { displayName: 'Caption', name: 'caption', type: 'string', default: '', displayOptions: { show: { operation: ['sendImage', 'sendDocument'] } }, description: 'Caption' },
36
+ { displayName: 'File Name', name: 'fileName', type: 'string', default: 'file', required: true, displayOptions: { show: { operation: ['sendDocument'] } }, description: 'File name' },
37
+ { displayName: 'Voice Note', name: 'ptt', type: 'boolean', default: false, displayOptions: { show: { operation: ['sendAudio'] } }, description: 'Voice note' },
38
+ { displayName: 'Latitude', name: 'latitude', type: 'number', default: 0, required: true, displayOptions: { show: { operation: ['sendLocation'] } }, description: 'Latitude' },
39
+ { displayName: 'Longitude', name: 'longitude', type: 'number', default: 0, required: true, displayOptions: { show: { operation: ['sendLocation'] } }, description: 'Longitude' },
104
40
  ],
105
41
  };
106
42
  }
@@ -109,60 +45,39 @@ class WhatsAppSend {
109
45
  const returnData = [];
110
46
  const credentials = await this.getCredentials('whatsappApi');
111
47
  const cfg = await (0, WhatsAppApiHelper_1.getWhatsAppCredentials)(credentials);
112
- // Ensure server is running
113
- await (0, WhatsAppApiHelper_1.ensureServerRunning)(cfg);
114
- const serverUrl = (0, WhatsAppApiHelper_1.getServerUrl)(cfg);
115
48
  for (let i = 0; i < items.length; i++) {
116
49
  try {
117
50
  const operation = this.getNodeParameter('operation', i);
118
51
  const recipient = this.getNodeParameter('recipient', i);
119
- let body = { to: recipient };
52
+ let content = {};
120
53
  switch (operation) {
121
54
  case 'sendText':
122
- body.text = this.getNodeParameter('text', i);
55
+ content = { text: this.getNodeParameter('text', i) };
123
56
  break;
124
57
  case 'sendImage': {
125
- const binaryProperty = this.getNodeParameter('binaryProperty', i);
126
- const buffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty);
127
- body.image = buffer.toString('base64');
128
- body.caption = this.getNodeParameter('caption', i) || undefined;
58
+ const bp = this.getNodeParameter('binaryProperty', i);
59
+ const buf = await this.helpers.getBinaryDataBuffer(i, bp);
60
+ content = { image: buf, caption: this.getNodeParameter('caption', i) || undefined };
129
61
  break;
130
62
  }
131
63
  case 'sendDocument': {
132
- const binaryProperty = this.getNodeParameter('binaryProperty', i);
133
- const buffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty);
134
- body.document = buffer.toString('base64');
135
- body.fileName = this.getNodeParameter('fileName', i);
136
- body.caption = this.getNodeParameter('caption', i) || undefined;
64
+ const bp = this.getNodeParameter('binaryProperty', i);
65
+ const buf = await this.helpers.getBinaryDataBuffer(i, bp);
66
+ content = { document: buf, fileName: this.getNodeParameter('fileName', i), caption: this.getNodeParameter('caption', i) || undefined };
137
67
  break;
138
68
  }
139
69
  case 'sendAudio': {
140
- const binaryProperty = this.getNodeParameter('binaryProperty', i);
141
- const buffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty);
142
- body.audio = buffer.toString('base64');
143
- body.ptt = this.getNodeParameter('ptt', i);
70
+ const bp = this.getNodeParameter('binaryProperty', i);
71
+ const buf = await this.helpers.getBinaryDataBuffer(i, bp);
72
+ content = { audio: buf, ptt: this.getNodeParameter('ptt', i) };
144
73
  break;
145
74
  }
146
75
  case 'sendLocation':
147
- body.location = {
148
- degreesLatitude: this.getNodeParameter('latitude', i),
149
- degreesLongitude: this.getNodeParameter('longitude', i),
150
- };
76
+ content = { location: { degreesLatitude: this.getNodeParameter('latitude', i), degreesLongitude: this.getNodeParameter('longitude', i) } };
151
77
  break;
152
78
  }
153
- const response = await fetch(`${serverUrl}/send`, {
154
- method: 'POST',
155
- headers: { 'Content-Type': 'application/json' },
156
- body: JSON.stringify(body),
157
- });
158
- const result = await response.json();
159
- if (!response.ok) {
160
- throw new Error(result.error || 'Send failed');
161
- }
162
- returnData.push({
163
- json: { ...result, operation, success: true },
164
- pairedItem: { item: i },
165
- });
79
+ const result = await (0, WhatsAppApiHelper_1.sendMessageWithAntiBan)(recipient, content, cfg);
80
+ returnData.push({ json: { ...result, operation, success: true }, pairedItem: { item: i } });
166
81
  }
167
82
  catch (error) {
168
83
  if (this.continueOnFail()) {
@@ -1,5 +1,5 @@
1
- import { INodeType, INodeTypeDescription, INodeExecutionData, IPollFunctions } from 'n8n-workflow';
1
+ import { INodeType, INodeTypeDescription, ITriggerFunctions, ITriggerResponse } from 'n8n-workflow';
2
2
  export declare class WhatsAppTrigger implements INodeType {
3
3
  description: INodeTypeDescription;
4
- poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null>;
4
+ trigger(this: ITriggerFunctions): Promise<ITriggerResponse>;
5
5
  }
@@ -11,12 +11,11 @@ class WhatsAppTrigger {
11
11
  group: ['trigger'],
12
12
  version: 1,
13
13
  subtitle: '={{$parameter["event"]}}',
14
- description: 'Triggers on incoming WhatsApp messages via background server',
14
+ description: 'Triggers on incoming WhatsApp messages connection stays alive while workflow is active',
15
15
  defaults: { name: 'WhatsApp Trigger' },
16
16
  inputs: [],
17
17
  outputs: ['main'],
18
18
  credentials: [{ name: 'whatsappApi', required: true }],
19
- polling: true,
20
19
  properties: [
21
20
  {
22
21
  displayName: 'Event',
@@ -44,37 +43,71 @@ class WhatsAppTrigger {
44
43
  ],
45
44
  };
46
45
  }
47
- async poll() {
46
+ async trigger() {
48
47
  const credentials = await this.getCredentials('whatsappApi');
49
48
  const cfg = await (0, WhatsAppApiHelper_1.getWhatsAppCredentials)(credentials);
50
49
  const chatFilter = this.getNodeParameter('chatJidFilter', '');
51
50
  const onlyText = this.getNodeParameter('onlyText', false);
52
- // Ensure server is running
53
- await (0, WhatsAppApiHelper_1.ensureServerRunning)(cfg);
54
- // Poll for messages
55
- try {
56
- const response = await fetch(`${(0, WhatsAppApiHelper_1.getServerUrl)(cfg)}/messages`);
57
- const data = await response.json();
58
- if (!data.messages || data.messages.length === 0)
59
- return null;
60
- // Apply filters
61
- let messages = data.messages;
62
- if (chatFilter) {
63
- messages = messages.filter((msg) => msg.chatJid === chatFilter);
51
+ // Connect to WhatsApp — this keeps the socket alive
52
+ const sock = await (0, WhatsAppApiHelper_1.ensureConnected)(cfg);
53
+ // Listen for incoming messages
54
+ sock.ev.on('messages.upsert', (upsert) => {
55
+ const { messages, type } = upsert;
56
+ if (type !== 'notify')
57
+ return;
58
+ for (const msg of messages) {
59
+ if (msg.key.fromMe)
60
+ continue;
61
+ if (chatFilter && msg.key.remoteJid !== chatFilter)
62
+ continue;
63
+ const parsed = (0, WhatsAppApiHelper_1.parseIncomingMessage)(msg);
64
+ if (!parsed)
65
+ continue;
66
+ if (onlyText && parsed.messageType !== 'conversation' && parsed.messageType !== 'extendedTextMessage')
67
+ continue;
68
+ // Emit message to workflow
69
+ this.emit([[{ json: parsed }]]);
64
70
  }
65
- if (onlyText) {
66
- messages = messages.filter((msg) => msg.messageType === 'conversation' || msg.messageType === 'extendedTextMessage');
71
+ });
72
+ // Handle connection errors
73
+ sock.ev.on('connection.update', (update) => {
74
+ if (update.connection === 'close') {
75
+ this.emitError(new Error('WhatsApp connection closed'));
67
76
  }
68
- if (messages.length === 0)
69
- return null;
70
- return [messages.map((msg) => ({
71
- json: msg,
72
- pairedItem: { item: 0 },
73
- }))];
74
- }
75
- catch {
76
- return null;
77
- }
77
+ });
78
+ // Cleanup when workflow deactivates
79
+ let closeFunctionCalled = false;
80
+ const closeFunction = async () => {
81
+ closeFunctionCalled = true;
82
+ try {
83
+ sock.end(new Error('Workflow deactivated'));
84
+ }
85
+ catch { }
86
+ };
87
+ // Manual trigger for testing
88
+ const manualTriggerFunction = async () => {
89
+ return new Promise((resolve) => {
90
+ const handler = (upsert) => {
91
+ for (const msg of upsert.messages) {
92
+ if (msg.key.fromMe)
93
+ continue;
94
+ const parsed = (0, WhatsAppApiHelper_1.parseIncomingMessage)(msg);
95
+ if (parsed) {
96
+ sock.ev.off('messages.upsert', handler);
97
+ this.emit([[{ json: parsed }]]);
98
+ resolve();
99
+ return;
100
+ }
101
+ }
102
+ };
103
+ sock.ev.on('messages.upsert', handler);
104
+ setTimeout(() => { sock.ev.off('messages.upsert', handler); resolve(); }, 30000);
105
+ });
106
+ };
107
+ return {
108
+ closeFunction,
109
+ manualTriggerFunction,
110
+ };
78
111
  }
79
112
  }
80
113
  exports.WhatsAppTrigger = WhatsAppTrigger;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raevon/n8n-nodes-whatsapp",
3
- "version": "2.0.14",
3
+ "version": "3.0.0",
4
4
  "description": "n8n community node for WhatsApp — send and receive messages with anti-ban protection via the Baileys library",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",