@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.
- package/dist/credentials/WhatsAppApi.credentials.js +5 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/nodes/WhatsApp/WhatsAppApiHelper.js +35 -14
- package/dist/nodes/WhatsApp/WhatsAppConnect.node.d.ts +5 -0
- package/dist/nodes/WhatsApp/WhatsAppConnect.node.js +89 -0
- package/dist/nodes/WhatsApp/WhatsAppTrigger.node.js +16 -8
- package/package.json +4 -5
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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: (
|
|
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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
79
|
+
buffer.push(parsed);
|
|
72
80
|
}
|
|
73
81
|
});
|
|
74
|
-
|
|
82
|
+
initializedBySession.add(sessionKey);
|
|
75
83
|
}
|
|
76
84
|
// Return buffered messages
|
|
77
|
-
if (
|
|
85
|
+
if (buffer.length === 0)
|
|
78
86
|
return null;
|
|
79
|
-
const messages =
|
|
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.
|
|
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",
|