@raevon/n8n-nodes-whatsapp 1.0.1 → 1.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.
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WhatsAppApi = void 0;
4
+ // Note: WhatsApp uses WebSocket (not HTTP), so n8n's ICredentialTestRequest
5
+ // can't test the connection. Session validation happens at socket connection
6
+ // time in ensureConnected(). If the session is invalid, the first node
7
+ // execution will trigger QR login automatically.
4
8
  class WhatsAppApi {
5
9
  constructor() {
6
10
  this.name = 'whatsappApi';
@@ -13,7 +17,7 @@ class WhatsAppApi {
13
17
  type: 'string',
14
18
  default: '~/.n8n/whatsapp-auth',
15
19
  required: true,
16
- description: 'Directory path where WhatsApp session files are stored. After first QR scan, session persists here.',
20
+ description: 'Directory path where WhatsApp session files are stored. After first QR scan, session persists here. Use the "WhatsApp Connect" node to scan QR on first setup.',
17
21
  },
18
22
  {
19
23
  displayName: 'QR Server Port',
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { WhatsAppSend } from './nodes/WhatsApp/WhatsAppSend.node';
2
2
  export { WhatsAppTrigger } from './nodes/WhatsApp/WhatsAppTrigger.node';
3
+ export { WhatsAppConnect } from './nodes/WhatsApp/WhatsAppConnect.node';
3
4
  export { WhatsAppApi } from './credentials/WhatsAppApi.credentials';
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.WhatsAppApi = exports.WhatsAppTrigger = exports.WhatsAppSend = void 0;
3
+ exports.WhatsAppApi = exports.WhatsAppConnect = exports.WhatsAppTrigger = exports.WhatsAppSend = void 0;
4
4
  var WhatsAppSend_node_1 = require("./nodes/WhatsApp/WhatsAppSend.node");
5
5
  Object.defineProperty(exports, "WhatsAppSend", { enumerable: true, get: function () { return WhatsAppSend_node_1.WhatsAppSend; } });
6
6
  var WhatsAppTrigger_node_1 = require("./nodes/WhatsApp/WhatsAppTrigger.node");
7
7
  Object.defineProperty(exports, "WhatsAppTrigger", { enumerable: true, get: function () { return WhatsAppTrigger_node_1.WhatsAppTrigger; } });
8
+ var WhatsAppConnect_node_1 = require("./nodes/WhatsApp/WhatsAppConnect.node");
9
+ Object.defineProperty(exports, "WhatsAppConnect", { enumerable: true, get: function () { return WhatsAppConnect_node_1.WhatsAppConnect; } });
8
10
  var WhatsAppApi_credentials_1 = require("./credentials/WhatsAppApi.credentials");
9
11
  Object.defineProperty(exports, "WhatsAppApi", { enumerable: true, get: function () { return WhatsAppApi_credentials_1.WhatsAppApi; } });
@@ -51,7 +51,17 @@ const node_http_1 = __importDefault(require("node:http"));
51
51
  const node_url_1 = require("node:url");
52
52
  const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
53
53
  const randBetween = (min, max) => max > min ? min + Math.floor(Math.random() * (max - min + 1)) : min;
54
+ // I8: Extracted ~ path expansion to avoid duplication
55
+ function expandHome(p) {
56
+ if (!p.startsWith('~'))
57
+ return p;
58
+ return node_path_1.default.join(process.env.HOME || process.env.USERPROFILE || '', p.slice(1));
59
+ }
54
60
  // --- Singleton socket manager ---
61
+ // NOTE: This singleton is shared across ALL n8n workflow executions using the same credential.
62
+ // Anti-ban counters (sentTodayCount, sentInBurst, nextSendAt) are module-level — two workflows
63
+ // burning through the same daily limit will share the counter. This is intentional for a
64
+ // single-credential setup. Multi-credential support would require a Map<sessionPath, State>.
55
65
  let socketInstance = null;
56
66
  let socketStatus = 'stopped';
57
67
  let socketConfig = null;
@@ -65,6 +75,7 @@ let reconnectAttempts = 0;
65
75
  let reconnectTimer = null;
66
76
  let qrResolve = null;
67
77
  let qrHttpServer = null;
78
+ let qrTimeout = null; // C3: Store timeout ref for cleanup
68
79
  let latestQr = null;
69
80
  let generation = 0; // #10: Generation counter — prevents stale handlers on reconnect
70
81
  function todayStartIso() {
@@ -116,6 +127,12 @@ function stopQrServer() {
116
127
  qrHttpServer = null;
117
128
  }
118
129
  }
130
+ function clearQrTimeout() {
131
+ if (qrTimeout) {
132
+ clearTimeout(qrTimeout);
133
+ qrTimeout = null;
134
+ }
135
+ }
119
136
  // #7: Reconnect backoff with jitter — exponential 1s→60s, ±20% randomization
120
137
  async function scheduleReconnect(cfg) {
121
138
  if (reconnectTimer)
@@ -132,9 +149,7 @@ async function initSocket(cfg, authPath) {
132
149
  if (socketInstance && socketStatus === 'connected')
133
150
  return socketInstance;
134
151
  const gen = ++generation; // #10: Snapshot generation for this connection attempt
135
- const resolvedPath = authPath.startsWith('~')
136
- ? node_path_1.default.join(process.env.HOME || process.env.USERPROFILE || '', authPath.slice(1))
137
- : authPath;
152
+ const resolvedPath = expandHome(authPath);
138
153
  if (!node_fs_1.default.existsSync(resolvedPath)) {
139
154
  node_fs_1.default.mkdirSync(resolvedPath, { recursive: true });
140
155
  }
@@ -161,7 +176,9 @@ async function initSocket(cfg, authPath) {
161
176
  sock.ev.on('creds.update', () => {
162
177
  if (gen !== generation)
163
178
  return; // #10: Ignore stale connection events
164
- saveCreds().catch(() => { });
179
+ saveCreds().catch((err) => {
180
+ console.error('[WhatsApp] Failed to save credentials:', err.message);
181
+ });
165
182
  });
166
183
  sock.ev.on('connection.update', (update) => {
167
184
  var _a, _b;
@@ -171,8 +188,8 @@ async function initSocket(cfg, authPath) {
171
188
  if (qr) {
172
189
  latestQr = qr;
173
190
  socketStatus = 'qr_ready';
174
- const qrUrl = `https://quickchart.io/qr?text=${encodeURIComponent(qr)}&size=300`;
175
191
  if (qrResolve) {
192
+ const qrUrl = `https://quickchart.io/qr?text=${encodeURIComponent(qr)}&size=300`;
176
193
  qrResolve({ qr, qrUrl });
177
194
  qrResolve = null;
178
195
  }
@@ -181,6 +198,7 @@ async function initSocket(cfg, authPath) {
181
198
  socketStatus = 'connected';
182
199
  reconnectAttempts = 0;
183
200
  latestQr = null;
201
+ clearQrTimeout(); // C3: Clear QR timeout on successful connection
184
202
  stopQrServer();
185
203
  return;
186
204
  }
@@ -233,11 +251,12 @@ async function ensureConnected(cfg) {
233
251
  };
234
252
  socketConfig = antiBanCfg;
235
253
  sessionPath = cfg.sessionPath;
236
- queue = new p_queue_1.default({ concurrency: 1 });
254
+ // C1: Guard queue creation only create once, preserve anti-ban state across calls
255
+ if (!queue) {
256
+ queue = new p_queue_1.default({ concurrency: 1 });
257
+ }
237
258
  // Start QR server for first-time setup
238
- const resolvedPath = cfg.sessionPath.startsWith('~')
239
- ? node_path_1.default.join(process.env.HOME || process.env.USERPROFILE || '', cfg.sessionPath.slice(1))
240
- : cfg.sessionPath;
259
+ const resolvedPath = expandHome(cfg.sessionPath);
241
260
  const hasSession = node_fs_1.default.existsSync(resolvedPath) && node_fs_1.default.readdirSync(resolvedPath).length > 0;
242
261
  if (!hasSession) {
243
262
  await startQrServer(cfg.qrPort);
@@ -247,12 +266,13 @@ async function ensureConnected(cfg) {
247
266
  if (!hasSession) {
248
267
  const qrData = await new Promise((resolve) => {
249
268
  qrResolve = resolve;
250
- // Timeout after 5 minutes
251
- setTimeout(() => {
269
+ // C3: Store timeout ref so it can be cleared on successful scan
270
+ qrTimeout = setTimeout(() => {
252
271
  if (qrResolve) {
253
272
  qrResolve(null);
254
273
  qrResolve = null;
255
274
  }
275
+ qrTimeout = null;
256
276
  }, 300000);
257
277
  });
258
278
  if (!qrData) {
@@ -402,7 +422,7 @@ function getConnectionStatus() {
402
422
  };
403
423
  }
404
424
  function parseIncomingMessage(msg) {
405
- var _a, _b, _c, _d, _e, _f, _g;
425
+ var _a, _b, _c, _d, _e, _f, _g, _h;
406
426
  if (!msg.message || !msg.key || !msg.key.remoteJid)
407
427
  return null;
408
428
  let content = null;
@@ -443,10 +463,11 @@ function parseIncomingMessage(msg) {
443
463
  return {
444
464
  messageId: msg.key.id,
445
465
  chatJid: msg.key.remoteJid,
446
- sender: msg.key.participant || (msg.key.remoteJid !== msg.key.participant ? msg.key.remoteJid : null),
466
+ // C4: Simplified sender logic participant is sender in groups, remoteJid in DMs
467
+ sender: (_g = msg.key.participant) !== null && _g !== void 0 ? _g : msg.key.remoteJid,
447
468
  content: content || `[${messageType}]`,
448
469
  timestamp: new Date(timestampSeconds * 1000).toISOString(),
449
- isFromMe: (_g = msg.key.fromMe) !== null && _g !== void 0 ? _g : false,
470
+ isFromMe: (_h = msg.key.fromMe) !== null && _h !== void 0 ? _h : false,
450
471
  messageType,
451
472
  };
452
473
  }
@@ -0,0 +1,5 @@
1
+ import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class WhatsAppConnect implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WhatsAppConnect = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const WhatsAppApiHelper_1 = require("./WhatsAppApiHelper");
6
+ class WhatsAppConnect {
7
+ constructor() {
8
+ this.description = {
9
+ displayName: 'WhatsApp Connect',
10
+ name: 'whatsAppConnect',
11
+ icon: 'file:icons/whatsapp.svg',
12
+ group: ['transform'],
13
+ version: 1,
14
+ subtitle: '={{$parameter["operation"]}}',
15
+ description: 'Connect to WhatsApp — scan QR code on first run, then reconnects automatically',
16
+ defaults: { name: 'WhatsApp Connect' },
17
+ inputs: ['main'],
18
+ outputs: ['main'],
19
+ credentials: [{ name: 'whatsappApi', required: true }],
20
+ properties: [
21
+ {
22
+ displayName: 'Operation',
23
+ name: 'operation',
24
+ type: 'options',
25
+ noDataExpression: true,
26
+ options: [
27
+ { name: 'Connect', value: 'connect', description: 'Connect to WhatsApp (scan QR on first run)', action: 'Connect to WhatsApp' },
28
+ { name: 'Get Status', value: 'status', description: 'Get current connection status', action: 'Get connection status' },
29
+ ],
30
+ default: 'connect',
31
+ },
32
+ ],
33
+ };
34
+ }
35
+ async execute() {
36
+ const items = this.getInputData();
37
+ const returnData = [];
38
+ const credentials = await this.getCredentials('whatsappApi');
39
+ const cfg = await (0, WhatsAppApiHelper_1.getWhatsAppCredentials)(credentials);
40
+ const operation = this.getNodeParameter('operation', 0);
41
+ for (let i = 0; i < items.length; i++) {
42
+ try {
43
+ if (operation === 'connect') {
44
+ // Connect to WhatsApp — this triggers QR flow on first run
45
+ const sock = await (0, WhatsAppApiHelper_1.ensureConnected)(cfg);
46
+ const status = (0, WhatsAppApiHelper_1.getConnectionStatus)();
47
+ returnData.push({
48
+ json: {
49
+ success: true,
50
+ status: status.status,
51
+ connected: status.connected,
52
+ message: status.connected
53
+ ? 'WhatsApp connected successfully'
54
+ : `QR code ready — open http://localhost:${cfg.qrPort}/qr in your browser to scan`,
55
+ qrUrl: status.qrAvailable ? `http://localhost:${cfg.qrPort}/qr` : null,
56
+ sessionPath: cfg.sessionPath,
57
+ },
58
+ pairedItem: { item: i },
59
+ });
60
+ }
61
+ else if (operation === 'status') {
62
+ const status = (0, WhatsAppApiHelper_1.getConnectionStatus)();
63
+ returnData.push({
64
+ json: {
65
+ ...status,
66
+ sessionPath: cfg.sessionPath,
67
+ },
68
+ pairedItem: { item: i },
69
+ });
70
+ }
71
+ }
72
+ catch (error) {
73
+ if (this.continueOnFail()) {
74
+ returnData.push({
75
+ json: {
76
+ error: error.message,
77
+ success: false,
78
+ },
79
+ pairedItem: { item: i },
80
+ });
81
+ continue;
82
+ }
83
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), error);
84
+ }
85
+ }
86
+ return [returnData];
87
+ }
88
+ }
89
+ exports.WhatsAppConnect = WhatsAppConnect;
@@ -2,8 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WhatsAppTrigger = void 0;
4
4
  const WhatsAppApiHelper_1 = require("./WhatsAppApiHelper");
5
- const messageBuffer = [];
6
- let socketInitialized = false;
5
+ // Per-credential buffers — one buffer per session path
6
+ // This ensures multiple trigger nodes with different credentials don't share messages
7
+ const buffersBySession = new Map();
8
+ const initializedBySession = new Set();
7
9
  class WhatsAppTrigger {
8
10
  constructor() {
9
11
  this.description = {
@@ -51,8 +53,14 @@ class WhatsAppTrigger {
51
53
  const cfg = await (0, WhatsAppApiHelper_1.getWhatsAppCredentials)(credentials);
52
54
  const chatFilter = this.getNodeParameter('chatJidFilter', '');
53
55
  const onlyText = this.getNodeParameter('onlyText', false);
54
- // Initialize socket listener once
55
- if (!socketInitialized) {
56
+ const sessionKey = cfg.sessionPath;
57
+ // Get or create buffer for this credential
58
+ if (!buffersBySession.has(sessionKey)) {
59
+ buffersBySession.set(sessionKey, []);
60
+ }
61
+ const buffer = buffersBySession.get(sessionKey);
62
+ // Initialize socket listener once per credential
63
+ if (!initializedBySession.has(sessionKey)) {
56
64
  const sock = await (0, WhatsAppApiHelper_1.ensureConnected)(cfg);
57
65
  sock.ev.on('messages.upsert', (upsert) => {
58
66
  const { messages, type } = upsert;
@@ -68,15 +76,15 @@ class WhatsAppTrigger {
68
76
  continue;
69
77
  if (onlyText && parsed.messageType !== 'conversation' && parsed.messageType !== 'extendedTextMessage')
70
78
  continue;
71
- messageBuffer.push(parsed);
79
+ buffer.push(parsed);
72
80
  }
73
81
  });
74
- socketInitialized = true;
82
+ initializedBySession.add(sessionKey);
75
83
  }
76
84
  // Return buffered messages
77
- if (messageBuffer.length === 0)
85
+ if (buffer.length === 0)
78
86
  return null;
79
- const messages = messageBuffer.splice(0, messageBuffer.length);
87
+ const messages = buffer.splice(0, buffer.length);
80
88
  return [messages.map((msg) => ({
81
89
  json: msg,
82
90
  pairedItem: { item: 0 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raevon/n8n-nodes-whatsapp",
3
- "version": "1.0.1",
3
+ "version": "1.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",
@@ -30,14 +30,13 @@
30
30
  ],
31
31
  "nodes": [
32
32
  "dist/nodes/WhatsApp/WhatsAppSend.node.js",
33
- "dist/nodes/WhatsApp/WhatsAppTrigger.node.js"
33
+ "dist/nodes/WhatsApp/WhatsAppTrigger.node.js",
34
+ "dist/nodes/WhatsApp/WhatsAppConnect.node.js"
34
35
  ]
35
36
  },
36
37
  "dependencies": {
37
38
  "@whiskeysockets/baileys": "^7.0.0-rc13",
38
- "p-queue": "^8.0.1",
39
- "pino": "^9.6.0",
40
- "open": "^10.1.0"
39
+ "p-queue": "^8.0.1"
41
40
  },
42
41
  "devDependencies": {
43
42
  "@types/node": "^20.19.39",