@raevon/n8n-nodes-whatsapp 2.0.1 → 2.0.3

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.
@@ -12,5 +12,4 @@ export interface WhatsAppCredentials {
12
12
  export declare function getServerUrl(cfg: WhatsAppCredentials): string;
13
13
  export declare function ensureServerRunning(cfg: WhatsAppCredentials): Promise<void>;
14
14
  export declare function disconnect(): Promise<void>;
15
- export declare function parseIncomingMessage(msg: any): Record<string, any> | null;
16
15
  export declare function getWhatsAppCredentials(credentials: Record<string, any>): Promise<WhatsAppCredentials>;
@@ -1,4 +1,37 @@
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
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -6,19 +39,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.getServerUrl = getServerUrl;
7
40
  exports.ensureServerRunning = ensureServerRunning;
8
41
  exports.disconnect = disconnect;
9
- exports.parseIncomingMessage = parseIncomingMessage;
10
42
  exports.getWhatsAppCredentials = getWhatsAppCredentials;
11
- const node_child_process_1 = require("node:child_process");
12
43
  const node_path_1 = __importDefault(require("node:path"));
13
44
  const node_fs_1 = __importDefault(require("node:fs"));
14
45
  const node_net_1 = __importDefault(require("node:net"));
46
+ const node_http_1 = __importDefault(require("node:http"));
47
+ const node_url_1 = require("node:url");
48
+ // Dynamic imports for Baileys (loaded only when server starts)
49
+ let baileysModule = null;
15
50
  function expandHome(p) {
16
51
  if (!p.startsWith('~'))
17
52
  return p;
18
53
  return node_path_1.default.join(process.env.HOME || process.env.USERPROFILE || '', p.slice(1));
19
54
  }
20
55
  function getPort(cfg) {
21
- // Derive port from session path to avoid conflicts between credentials
22
56
  const hash = cfg.sessionPath.split('').reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0);
23
57
  return 3456 + (Math.abs(hash) % 1000);
24
58
  }
@@ -33,124 +67,288 @@ async function isPortOpen(port) {
33
67
  server.listen(port, '127.0.0.1');
34
68
  });
35
69
  }
36
- let serverProcess = null;
37
- let serverPort = 0;
38
- async function ensureServerRunning(cfg) {
70
+ // --- In-memory server state (singleton per process) ---
71
+ const servers = new Map();
72
+ async function startServer(cfg) {
39
73
  const port = getPort(cfg);
40
- // Check if server is already running
41
- if (await isPortOpen(port)) {
42
- serverPort = port;
74
+ // Already running?
75
+ if (servers.has(port))
43
76
  return;
77
+ // Load Baileys dynamically
78
+ if (!baileysModule) {
79
+ baileysModule = await Promise.resolve().then(() => __importStar(require('@whiskeysockets/baileys')));
44
80
  }
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),
81
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
82
+ const randBetween = (min, max) => max > min ? min + Math.floor(Math.random() * (max - min + 1)) : min;
83
+ const noop = () => { };
84
+ const silentLogger = { level: 'silent', info: noop, warn: noop, error: noop, debug: noop, fatal: noop, trace: noop, child: () => silentLogger };
85
+ const state = {
86
+ server: null,
87
+ status: 'stopped',
88
+ qr: null,
89
+ socket: null,
90
+ messageBuffer: [],
91
+ queue: null,
92
+ nextSendAt: 0,
93
+ sentInBurst: 0,
94
+ sentTodayCount: 0,
95
+ sentTodayDate: '',
62
96
  };
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' });
97
+ async function connect() {
98
+ if (state.socket && state.status === 'connected')
99
+ return;
100
+ const resolvedPath = expandHome(cfg.sessionPath);
101
+ if (!node_fs_1.default.existsSync(resolvedPath))
102
+ node_fs_1.default.mkdirSync(resolvedPath, { recursive: true });
103
+ const { useMultiFileAuthState, fetchLatestBaileysVersion, makeWASocket, makeCacheableSignalKeyStore, Browsers, DisconnectReason } = baileysModule;
104
+ const { state: authState, saveCreds } = await useMultiFileAuthState(resolvedPath);
105
+ const { version } = await fetchLatestBaileysVersion();
106
+ state.status = 'connecting';
107
+ state.qr = null;
108
+ state.socket = makeWASocket({
109
+ version,
110
+ browser: Browsers.ubuntu('n8n WhatsApp Node'),
111
+ auth: { creds: authState.creds, keys: makeCacheableSignalKeyStore(authState.keys, silentLogger) },
112
+ markOnlineOnConnect: false,
113
+ syncFullHistory: false,
114
+ shouldSyncHistoryMessage: () => false,
115
+ generateHighQualityLinkPreview: false,
116
+ logger: silentLogger,
117
+ });
118
+ state.socket.ev.on('creds.update', () => saveCreds().catch(() => { }));
119
+ state.socket.ev.on('connection.update', (update) => {
120
+ var _a, _b;
121
+ const { connection, lastDisconnect, qr: newQr } = update;
122
+ if (newQr) {
123
+ state.qr = newQr;
124
+ state.status = 'qr_ready';
125
+ }
126
+ if (connection === 'open') {
127
+ state.status = 'connected';
128
+ state.qr = null;
129
+ return;
130
+ }
131
+ if (connection === 'close') {
132
+ 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;
133
+ if (code === DisconnectReason.loggedOut) {
134
+ state.status = 'logged_out';
135
+ state.socket = null;
136
+ }
137
+ else {
138
+ state.status = 'disconnected';
139
+ state.socket = null;
140
+ setTimeout(() => connect().catch(() => { }), 5000);
141
+ }
142
+ }
143
+ });
144
+ state.socket.ev.on('messages.upsert', (upsert) => {
145
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
146
+ const { messages, type } = upsert;
147
+ if (type !== 'notify')
148
+ return;
149
+ for (const msg of messages) {
150
+ if (msg.key.fromMe)
151
+ continue;
152
+ let content = null;
153
+ const msgType = Object.keys(msg.message || {})[0] || 'unknown';
154
+ if ((_a = msg.message) === null || _a === void 0 ? void 0 : _a.conversation)
155
+ content = msg.message.conversation;
156
+ else if ((_c = (_b = msg.message) === null || _b === void 0 ? void 0 : _b.extendedTextMessage) === null || _c === void 0 ? void 0 : _c.text)
157
+ content = msg.message.extendedTextMessage.text;
158
+ else if ((_e = (_d = msg.message) === null || _d === void 0 ? void 0 : _d.imageMessage) === null || _e === void 0 ? void 0 : _e.caption)
159
+ content = msg.message.imageMessage.caption;
160
+ else if ((_g = (_f = msg.message) === null || _f === void 0 ? void 0 : _f.videoMessage) === null || _g === void 0 ? void 0 : _g.caption)
161
+ content = msg.message.videoMessage.caption;
162
+ else if ((_j = (_h = msg.message) === null || _h === void 0 ? void 0 : _h.documentMessage) === null || _j === void 0 ? void 0 : _j.fileName)
163
+ content = msg.message.documentMessage.fileName;
164
+ else if ((_k = msg.message) === null || _k === void 0 ? void 0 : _k.audioMessage)
165
+ content = '[Audio]';
166
+ else
167
+ content = `[${msgType}]`;
168
+ const ts = msg.messageTimestamp != null ? Number(msg.messageTimestamp) : Date.now() / 1000;
169
+ 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 });
170
+ }
171
+ });
172
+ if (!state.queue) {
173
+ const PQueue = (await Promise.resolve().then(() => __importStar(require('p-queue')))).default;
174
+ state.queue = new PQueue({ concurrency: 1 });
175
+ }
67
176
  }
68
- catch (err) {
69
- throw new Error(`Failed to start WhatsApp server: ${err.message}`);
177
+ function normalizeRecipient(to) {
178
+ if (to.endsWith('@s.whatsapp.net') || to.endsWith('@g.us'))
179
+ return to;
180
+ const digits = to.replace(/\D/g, '');
181
+ if (!/^[1-9][0-9]{7,14}$/.test(digits))
182
+ throw new Error(`Invalid phone number: ${to}`);
183
+ return `${digits}@s.whatsapp.net`;
70
184
  }
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)) {
77
- clearInterval(check);
78
- resolve(true);
185
+ async function sendMessage(to, content) {
186
+ var _a;
187
+ if (!state.socket || state.status !== 'connected')
188
+ throw new Error('WhatsApp not connected');
189
+ if (cfg.dailySendLimit > 0) {
190
+ const today = new Date().toISOString().slice(0, 10) + 'T00:00:00.000Z';
191
+ if (state.sentTodayDate !== today) {
192
+ state.sentTodayDate = today;
193
+ state.sentTodayCount = 0;
79
194
  }
80
- if (Date.now() - start > 10000) {
81
- clearInterval(check);
82
- resolve(false);
195
+ if (state.sentTodayCount >= cfg.dailySendLimit)
196
+ throw new Error(`Daily limit (${cfg.dailySendLimit}) reached`);
197
+ }
198
+ const jid = normalizeRecipient(to);
199
+ if (cfg.checkRecipientExists && !jid.endsWith('@g.us')) {
200
+ const results = await state.socket.onWhatsApp(jid);
201
+ if (!((_a = results === null || results === void 0 ? void 0 : results[0]) === null || _a === void 0 ? void 0 : _a.exists))
202
+ throw new Error(`Recipient ${to} not on WhatsApp`);
203
+ }
204
+ const task = state.queue.add(async () => {
205
+ var _a;
206
+ const wait = state.nextSendAt - Date.now();
207
+ if (wait > 0)
208
+ await sleep(wait);
209
+ if (cfg.typingSimulation) {
210
+ try {
211
+ await state.socket.sendPresenceUpdate('composing', jid);
212
+ const len = (content.text || content.caption || '').length;
213
+ await sleep(Math.min(randBetween(900, 1800) + len * 25, 4000));
214
+ await state.socket.sendPresenceUpdate('paused', jid);
215
+ }
216
+ catch { }
83
217
  }
84
- }, 500);
85
- });
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');
218
+ const response = await state.socket.sendMessage(jid, content);
219
+ const gap = randBetween(cfg.messageDelayMinMs, cfg.messageDelayMaxMs);
220
+ let burst = 0;
221
+ if (cfg.burstSize && ++state.sentInBurst >= cfg.burstSize) {
222
+ state.sentInBurst = 0;
223
+ burst = randBetween(cfg.burstPauseMinMs, cfg.burstPauseMaxMs);
224
+ }
225
+ state.nextSendAt = Date.now() + gap + burst;
226
+ if (cfg.dailySendLimit > 0)
227
+ state.sentTodayCount++;
228
+ if (!((_a = response === null || response === void 0 ? void 0 : response.key) === null || _a === void 0 ? void 0 : _a.id))
229
+ throw new Error('No message ID returned');
230
+ return { messageId: response.key.id, status: 'sent', recipient: jid };
231
+ });
232
+ let timer;
233
+ const winner = await Promise.race([
234
+ task.then((r) => ({ result: r }), (e) => ({ error: e })),
235
+ new Promise(r => { timer = setTimeout(() => r(null), 15000); }),
236
+ ]);
237
+ clearTimeout(timer);
238
+ if (!winner) {
239
+ task.then(() => { }, () => { });
240
+ return { messageId: 'queued', status: 'queued', recipient: jid };
91
241
  }
92
- catch { }
93
- throw new Error(`WhatsApp server failed to start. Log: ${logContent.slice(0, 500)}`);
242
+ if (winner.error)
243
+ throw winner.error;
244
+ return winner.result;
94
245
  }
95
- }
96
- async function disconnect() {
97
- // Signal server to stop (it's a separate process)
98
- if (serverPort) {
246
+ // HTTP server
247
+ const httpServer = node_http_1.default.createServer(async (req, res) => {
248
+ const url = new node_url_1.URL(req.url || '/', `http://localhost:${port}`);
249
+ res.setHeader('Access-Control-Allow-Origin', '*');
250
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
251
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
252
+ if (req.method === 'OPTIONS') {
253
+ res.writeHead(204);
254
+ res.end();
255
+ return;
256
+ }
99
257
  try {
100
- await fetch(`http://127.0.0.1:${serverPort}/health`);
258
+ if (url.pathname === '/health') {
259
+ res.writeHead(200, { 'Content-Type': 'application/json' });
260
+ res.end(JSON.stringify({ ok: true, status: state.status, connected: state.status === 'connected' }));
261
+ }
262
+ else if (url.pathname === '/status') {
263
+ res.writeHead(200, { 'Content-Type': 'application/json' });
264
+ res.end(JSON.stringify({ status: state.status, connected: state.status === 'connected', qrAvailable: state.status === 'qr_ready', sentToday: state.sentTodayCount, dailyLimit: cfg.dailySendLimit }));
265
+ }
266
+ else if (url.pathname === '/connect') {
267
+ await connect();
268
+ const result = await new Promise(r => {
269
+ const check = setInterval(() => {
270
+ if (state.status === 'connected') {
271
+ clearInterval(check);
272
+ r({ connected: true, message: 'Connected' });
273
+ }
274
+ if (state.qr) {
275
+ clearInterval(check);
276
+ 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' })));
277
+ }
278
+ }, 500);
279
+ setTimeout(() => { clearInterval(check); r({ connected: false, status: state.status, message: 'Timeout' }); }, 30000);
280
+ });
281
+ res.writeHead(200, { 'Content-Type': 'application/json' });
282
+ res.end(JSON.stringify(result));
283
+ }
284
+ else if (url.pathname === '/qr' && state.qr) {
285
+ const QRCode = await Promise.resolve().then(() => __importStar(require('qrcode')));
286
+ const qrUrl = await QRCode.toDataURL(state.qr, { width: 400, margin: 2 });
287
+ res.writeHead(200, { 'Content-Type': 'text/html' });
288
+ res.end(`<!DOCTYPE html><html><body style="display:flex;justify-content:center;align-items:center;height:100vh;background:#111"><img src="${qrUrl}"/></body></html>`);
289
+ }
290
+ else if (url.pathname === '/send' && req.method === 'POST') {
291
+ let body = '';
292
+ req.on('data', chunk => body += chunk);
293
+ req.on('end', async () => {
294
+ try {
295
+ const { to, ...content } = JSON.parse(body);
296
+ if (!to)
297
+ throw new Error('Missing "to"');
298
+ const result = await sendMessage(to, content);
299
+ res.writeHead(200, { 'Content-Type': 'application/json' });
300
+ res.end(JSON.stringify(result));
301
+ }
302
+ catch (err) {
303
+ res.writeHead(400, { 'Content-Type': 'application/json' });
304
+ res.end(JSON.stringify({ error: err.message }));
305
+ }
306
+ });
307
+ }
308
+ else if (url.pathname === '/messages') {
309
+ const msgs = state.messageBuffer.splice(0, state.messageBuffer.length);
310
+ res.writeHead(200, { 'Content-Type': 'application/json' });
311
+ res.end(JSON.stringify({ messages: msgs, count: msgs.length }));
312
+ }
313
+ else if (url.pathname === '/signout') {
314
+ if (state.socket) {
315
+ try {
316
+ state.socket.end(new Error('Sign out'));
317
+ }
318
+ catch { }
319
+ state.socket = null;
320
+ }
321
+ state.status = 'stopped';
322
+ const p = expandHome(cfg.sessionPath);
323
+ if (node_fs_1.default.existsSync(p)) {
324
+ node_fs_1.default.readdirSync(p).forEach(f => node_fs_1.default.unlinkSync(node_path_1.default.join(p, f)));
325
+ node_fs_1.default.rmdirSync(p);
326
+ }
327
+ res.writeHead(200, { 'Content-Type': 'application/json' });
328
+ res.end(JSON.stringify({ success: true, message: 'Signed out' }));
329
+ }
330
+ else {
331
+ res.writeHead(404);
332
+ res.end('Not found');
333
+ }
101
334
  }
102
- catch { }
103
- }
335
+ catch (err) {
336
+ res.writeHead(500, { 'Content-Type': 'application/json' });
337
+ res.end(JSON.stringify({ error: err.message }));
338
+ }
339
+ });
340
+ await new Promise(r => httpServer.listen(port, () => r()));
341
+ state.server = httpServer;
342
+ servers.set(port, state);
343
+ console.log(`[WhatsApp] Server started on port ${port}`);
104
344
  }
105
- function parseIncomingMessage(msg) {
106
- var _a, _b, _c, _d, _e, _f, _g, _h;
107
- if (!msg.message || !msg.key || !msg.key.remoteJid)
108
- return null;
109
- let content = null;
110
- const messageType = Object.keys(msg.message)[0];
111
- if (msg.message.conversation) {
112
- content = msg.message.conversation;
113
- }
114
- else if ((_a = msg.message.extendedTextMessage) === null || _a === void 0 ? void 0 : _a.text) {
115
- content = msg.message.extendedTextMessage.text;
116
- }
117
- else if ((_b = msg.message.imageMessage) === null || _b === void 0 ? void 0 : _b.caption) {
118
- content = msg.message.imageMessage.caption;
119
- }
120
- else if ((_c = msg.message.videoMessage) === null || _c === void 0 ? void 0 : _c.caption) {
121
- content = msg.message.videoMessage.caption;
122
- }
123
- else if ((_d = msg.message.documentMessage) === null || _d === void 0 ? void 0 : _d.fileName) {
124
- content = msg.message.documentMessage.fileName;
125
- }
126
- else if (msg.message.audioMessage) {
127
- content = '[Audio]';
128
- }
129
- else if (msg.message.stickerMessage) {
130
- content = '[Sticker]';
131
- }
132
- else if (msg.message.locationMessage) {
133
- content = `[Location] ${msg.message.locationMessage.address || ''}`;
134
- }
135
- else if ((_e = msg.message.contactMessage) === null || _e === void 0 ? void 0 : _e.displayName) {
136
- content = `[Contact] ${msg.message.contactMessage.displayName}`;
137
- }
138
- else if ((_f = msg.message.pollCreationMessage) === null || _f === void 0 ? void 0 : _f.name) {
139
- content = `[Poll] ${msg.message.pollCreationMessage.name}`;
345
+ async function ensureServerRunning(cfg) {
346
+ const port = getPort(cfg);
347
+ if (!servers.has(port)) {
348
+ await startServer(cfg);
140
349
  }
141
- const timestampSeconds = msg.messageTimestamp != null
142
- ? Number(msg.messageTimestamp)
143
- : Date.now() / 1000;
144
- return {
145
- messageId: msg.key.id,
146
- chatJid: msg.key.remoteJid,
147
- sender: (_g = msg.key.participant) !== null && _g !== void 0 ? _g : msg.key.remoteJid,
148
- content: content || `[${messageType}]`,
149
- timestamp: new Date(timestampSeconds * 1000).toISOString(),
150
- isFromMe: (_h = msg.key.fromMe) !== null && _h !== void 0 ? _h : false,
151
- messageType,
152
- };
153
350
  }
351
+ async function disconnect() { }
154
352
  async function getWhatsAppCredentials(credentials) {
155
353
  var _a, _b;
156
354
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raevon/n8n-nodes-whatsapp",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
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",