@raevon/n8n-nodes-whatsapp 1.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.
@@ -0,0 +1,7 @@
1
+ import { ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class WhatsAppApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WhatsAppApi = void 0;
4
+ class WhatsAppApi {
5
+ constructor() {
6
+ this.name = 'whatsappApi';
7
+ this.displayName = 'WhatsApp API';
8
+ this.documentationUrl = 'https://github.com/WhiskeySockets/Baileys';
9
+ this.properties = [
10
+ {
11
+ displayName: 'Session Path',
12
+ name: 'sessionPath',
13
+ type: 'string',
14
+ default: '~/.n8n/whatsapp-auth',
15
+ required: true,
16
+ description: 'Directory path where WhatsApp session files are stored. After first QR scan, session persists here.',
17
+ },
18
+ {
19
+ displayName: 'QR Server Port',
20
+ name: 'qrPort',
21
+ type: 'number',
22
+ default: 3456,
23
+ description: 'Local HTTP server port for serving QR code during first-time setup. Only used when no session exists.',
24
+ },
25
+ {
26
+ displayName: 'Message Delay Min (ms)',
27
+ name: 'messageDelayMinMs',
28
+ type: 'number',
29
+ default: 5000,
30
+ description: 'Minimum delay between messages in milliseconds. Part of anti-ban protection.',
31
+ },
32
+ {
33
+ displayName: 'Message Delay Max (ms)',
34
+ name: 'messageDelayMaxMs',
35
+ type: 'number',
36
+ default: 9000,
37
+ description: 'Maximum delay between messages in milliseconds. Part of anti-ban protection.',
38
+ },
39
+ {
40
+ displayName: 'Burst Size',
41
+ name: 'burstSize',
42
+ type: 'number',
43
+ default: 20,
44
+ description: 'Number of messages before an extra cooldown pause. Set to 0 to disable.',
45
+ },
46
+ {
47
+ displayName: 'Burst Pause Min (ms)',
48
+ name: 'burstPauseMinMs',
49
+ type: 'number',
50
+ default: 30000,
51
+ description: 'Minimum burst cooldown pause in milliseconds.',
52
+ },
53
+ {
54
+ displayName: 'Burst Pause Max (ms)',
55
+ name: 'burstPauseMaxMs',
56
+ type: 'number',
57
+ default: 60000,
58
+ description: 'Maximum burst cooldown pause in milliseconds.',
59
+ },
60
+ {
61
+ displayName: 'Typing Simulation',
62
+ name: 'typingSimulation',
63
+ type: 'boolean',
64
+ default: true,
65
+ description: 'Show "typing..." indicator before sending each message to appear human-like.',
66
+ },
67
+ {
68
+ displayName: 'Daily Send Limit',
69
+ name: 'dailySendLimit',
70
+ type: 'number',
71
+ default: 500,
72
+ description: 'Maximum messages per day (UTC). Set to 0 for unlimited (not recommended).',
73
+ },
74
+ {
75
+ displayName: 'Check Recipient Exists',
76
+ name: 'checkRecipientExists',
77
+ type: 'boolean',
78
+ default: true,
79
+ description: 'Verify the recipient phone number is registered on WhatsApp before sending.',
80
+ },
81
+ ];
82
+ }
83
+ }
84
+ exports.WhatsAppApi = WhatsAppApi;
@@ -0,0 +1,3 @@
1
+ export { WhatsAppSend } from './nodes/WhatsApp/WhatsAppSend.node';
2
+ export { WhatsAppTrigger } from './nodes/WhatsApp/WhatsAppTrigger.node';
3
+ export { WhatsAppApi } from './credentials/WhatsAppApi.credentials';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WhatsAppApi = exports.WhatsAppTrigger = exports.WhatsAppSend = void 0;
4
+ var WhatsAppSend_node_1 = require("./nodes/WhatsApp/WhatsAppSend.node");
5
+ Object.defineProperty(exports, "WhatsAppSend", { enumerable: true, get: function () { return WhatsAppSend_node_1.WhatsAppSend; } });
6
+ var WhatsAppTrigger_node_1 = require("./nodes/WhatsApp/WhatsAppTrigger.node");
7
+ Object.defineProperty(exports, "WhatsAppTrigger", { enumerable: true, get: function () { return WhatsAppTrigger_node_1.WhatsAppTrigger; } });
8
+ var WhatsAppApi_credentials_1 = require("./credentials/WhatsAppApi.credentials");
9
+ Object.defineProperty(exports, "WhatsAppApi", { enumerable: true, get: function () { return WhatsAppApi_credentials_1.WhatsAppApi; } });
@@ -0,0 +1,59 @@
1
+ import { type WASocket } from '@whiskeysockets/baileys';
2
+ export interface WhatsAppCredentials {
3
+ sessionPath: string;
4
+ qrPort: number;
5
+ messageDelayMinMs: number;
6
+ messageDelayMaxMs: number;
7
+ burstSize: number;
8
+ burstPauseMinMs: number;
9
+ burstPauseMaxMs: number;
10
+ typingSimulation: boolean;
11
+ dailySendLimit: number;
12
+ checkRecipientExists: boolean;
13
+ }
14
+ interface AntiBanConfig {
15
+ messageDelayMinMs: number;
16
+ messageDelayMaxMs: number;
17
+ burstSize: number;
18
+ burstPauseMinMs: number;
19
+ burstPauseMaxMs: number;
20
+ typingSimulation: boolean;
21
+ dailySendLimit: number;
22
+ checkRecipientExists: boolean;
23
+ }
24
+ export declare function getWhatsAppCredentials(credentials: Record<string, any>): Promise<WhatsAppCredentials>;
25
+ export declare function ensureConnected(cfg: WhatsAppCredentials): Promise<WASocket>;
26
+ export declare function simulateTyping(jid: string, content: {
27
+ text?: string;
28
+ caption?: string;
29
+ }, cfg: AntiBanConfig): Promise<void>;
30
+ export declare function sendMessageWithAntiBan(to: string, content: {
31
+ text?: string;
32
+ image?: Buffer;
33
+ document?: Buffer;
34
+ audio?: Buffer;
35
+ location?: {
36
+ degreesLatitude: number;
37
+ degreesLongitude: number;
38
+ name?: string;
39
+ address?: string;
40
+ };
41
+ mimetype?: string;
42
+ fileName?: string;
43
+ caption?: string;
44
+ ptt?: boolean;
45
+ }, cfg: WhatsAppCredentials): Promise<{
46
+ messageId: string;
47
+ status: string;
48
+ recipient: string;
49
+ }>;
50
+ export declare function getConnectionStatus(): {
51
+ status: string;
52
+ connected: boolean;
53
+ qrAvailable: boolean;
54
+ queueSize: number;
55
+ sentToday: number;
56
+ dailyLimit: number;
57
+ };
58
+ export declare function parseIncomingMessage(msg: any): Record<string, any> | null;
59
+ export {};
@@ -0,0 +1,423 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.getWhatsAppCredentials = getWhatsAppCredentials;
40
+ exports.ensureConnected = ensureConnected;
41
+ exports.simulateTyping = simulateTyping;
42
+ exports.sendMessageWithAntiBan = sendMessageWithAntiBan;
43
+ exports.getConnectionStatus = getConnectionStatus;
44
+ exports.parseIncomingMessage = parseIncomingMessage;
45
+ const n8n_workflow_1 = require("n8n-workflow");
46
+ const baileys_1 = __importStar(require("@whiskeysockets/baileys"));
47
+ const p_queue_1 = __importDefault(require("p-queue"));
48
+ const node_path_1 = __importDefault(require("node:path"));
49
+ const node_fs_1 = __importDefault(require("node:fs"));
50
+ const node_http_1 = __importDefault(require("node:http"));
51
+ const node_url_1 = require("node:url");
52
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
53
+ const randBetween = (min, max) => max > min ? min + Math.floor(Math.random() * (max - min + 1)) : min;
54
+ // --- Singleton socket manager ---
55
+ let socketInstance = null;
56
+ let socketStatus = 'stopped';
57
+ let socketConfig = null;
58
+ let sessionPath = null;
59
+ let queue = null;
60
+ let nextSendAt = 0;
61
+ let sentInBurst = 0;
62
+ let sentTodayCount = 0;
63
+ let sentTodayDate = '';
64
+ let reconnectAttempts = 0;
65
+ let reconnectTimer = null;
66
+ let qrResolve = null;
67
+ let qrHttpServer = null;
68
+ let latestQr = null;
69
+ function todayStartIso() {
70
+ return new Date().toISOString().slice(0, 10) + 'T00:00:00.000Z';
71
+ }
72
+ function sendGapMs(cfg) {
73
+ return randBetween(cfg.messageDelayMinMs, cfg.messageDelayMaxMs);
74
+ }
75
+ function burstPauseMs(cfg) {
76
+ const size = cfg.burstSize;
77
+ if (!size || ++sentInBurst < size)
78
+ return 0;
79
+ sentInBurst = 0;
80
+ return randBetween(cfg.burstPauseMinMs, cfg.burstPauseMaxMs);
81
+ }
82
+ function normalizeRecipient(to) {
83
+ if (to.endsWith('@s.whatsapp.net') || to.endsWith('@g.us'))
84
+ return to;
85
+ const digits = to.replace(/\D/g, '');
86
+ if (!/^[1-9][0-9]{7,14}$/.test(digits)) {
87
+ throw new Error(`Invalid phone number: ${to}`);
88
+ }
89
+ return `${digits}@s.whatsapp.net`;
90
+ }
91
+ function startQrServer(port) {
92
+ return new Promise((resolve) => {
93
+ if (qrHttpServer) {
94
+ resolve();
95
+ return;
96
+ }
97
+ qrHttpServer = node_http_1.default.createServer((req, res) => {
98
+ const url = new node_url_1.URL(req.url || '/', `http://localhost:${port}`);
99
+ if (url.pathname === '/qr' && latestQr) {
100
+ const qrUrl = `https://quickchart.io/qr?text=${encodeURIComponent(latestQr)}&size=300`;
101
+ res.writeHead(200, { 'Content-Type': 'text/html' });
102
+ res.end(`<!DOCTYPE html><html><head><title>WhatsApp QR</title><style>body{display:flex;justify-content:center;align-items:center;height:100vh;margin:0;background:#111;color:white;font-family:sans-serif}img{border-radius:12px}p{margin-top:20px;text-align:center}</style></head><body><div><img src="${qrUrl}" width="300"/><p>Scan with WhatsApp → Settings → Linked Devices → Link a Device</p><p style="color:#888;font-size:12px">This page auto-refreshes. Session saves after scan.</p></div></body></html>`);
103
+ }
104
+ else {
105
+ res.writeHead(404);
106
+ res.end('Not found');
107
+ }
108
+ });
109
+ qrHttpServer.listen(port, () => resolve());
110
+ });
111
+ }
112
+ function stopQrServer() {
113
+ if (qrHttpServer) {
114
+ qrHttpServer.close();
115
+ qrHttpServer = null;
116
+ }
117
+ }
118
+ async function scheduleReconnect(cfg) {
119
+ if (reconnectTimer)
120
+ return;
121
+ const base = Math.min(1000 * (2 ** reconnectAttempts), 60000);
122
+ const delay = Math.round(base * (0.8 + Math.random() * 0.4));
123
+ reconnectAttempts += 1;
124
+ reconnectTimer = setTimeout(() => {
125
+ reconnectTimer = null;
126
+ initSocket(cfg, sessionPath).catch(() => { });
127
+ }, delay);
128
+ }
129
+ async function initSocket(cfg, authPath) {
130
+ if (socketInstance && socketStatus === 'connected')
131
+ return socketInstance;
132
+ const resolvedPath = authPath.startsWith('~')
133
+ ? node_path_1.default.join(process.env.HOME || process.env.USERPROFILE || '', authPath.slice(1))
134
+ : authPath;
135
+ if (!node_fs_1.default.existsSync(resolvedPath)) {
136
+ node_fs_1.default.mkdirSync(resolvedPath, { recursive: true });
137
+ }
138
+ const { state, saveCreds } = await (0, baileys_1.useMultiFileAuthState)(resolvedPath);
139
+ const { version } = await (0, baileys_1.fetchLatestBaileysVersion)();
140
+ const sock = (0, baileys_1.default)({
141
+ version,
142
+ browser: baileys_1.Browsers.ubuntu('n8n WhatsApp Node'),
143
+ auth: {
144
+ creds: state.creds,
145
+ keys: (0, baileys_1.makeCacheableSignalKeyStore)(state.keys, { level: 'silent' }),
146
+ },
147
+ markOnlineOnConnect: false,
148
+ syncFullHistory: false,
149
+ shouldSyncHistoryMessage: () => false,
150
+ generateHighQualityLinkPreview: false,
151
+ logger: { level: 'silent' },
152
+ });
153
+ socketInstance = sock;
154
+ socketStatus = 'connecting';
155
+ reconnectAttempts = 0;
156
+ sock.ev.on('creds.update', () => {
157
+ saveCreds().catch(() => { });
158
+ });
159
+ sock.ev.on('connection.update', (update) => {
160
+ var _a, _b;
161
+ const { connection, lastDisconnect, qr } = update;
162
+ if (qr) {
163
+ latestQr = qr;
164
+ socketStatus = 'qr_ready';
165
+ const qrUrl = `https://quickchart.io/qr?text=${encodeURIComponent(qr)}&size=300`;
166
+ if (qrResolve) {
167
+ qrResolve({ qr, qrUrl });
168
+ qrResolve = null;
169
+ }
170
+ }
171
+ if (connection === 'open') {
172
+ socketStatus = 'connected';
173
+ reconnectAttempts = 0;
174
+ latestQr = null;
175
+ stopQrServer();
176
+ return;
177
+ }
178
+ if (connection === 'close') {
179
+ 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;
180
+ if (code === baileys_1.DisconnectReason.loggedOut) {
181
+ socketStatus = 'logged_out';
182
+ socketInstance = null;
183
+ stopQrServer();
184
+ }
185
+ else {
186
+ socketInstance = null;
187
+ scheduleReconnect(cfg);
188
+ }
189
+ }
190
+ });
191
+ return sock;
192
+ }
193
+ // --- Public API ---
194
+ async function getWhatsAppCredentials(credentials) {
195
+ var _a, _b;
196
+ return {
197
+ sessionPath: credentials.sessionPath || '~/.n8n/whatsapp-auth',
198
+ qrPort: credentials.qrPort || 3456,
199
+ messageDelayMinMs: credentials.messageDelayMinMs || 5000,
200
+ messageDelayMaxMs: credentials.messageDelayMaxMs || 9000,
201
+ burstSize: (_a = credentials.burstSize) !== null && _a !== void 0 ? _a : 20,
202
+ burstPauseMinMs: credentials.burstPauseMinMs || 30000,
203
+ burstPauseMaxMs: credentials.burstPauseMaxMs || 60000,
204
+ typingSimulation: credentials.typingSimulation !== false,
205
+ dailySendLimit: (_b = credentials.dailySendLimit) !== null && _b !== void 0 ? _b : 500,
206
+ checkRecipientExists: credentials.checkRecipientExists !== false,
207
+ };
208
+ }
209
+ async function ensureConnected(cfg) {
210
+ if (socketStatus === 'connected' && socketInstance)
211
+ return socketInstance;
212
+ const antiBanCfg = {
213
+ messageDelayMinMs: cfg.messageDelayMinMs,
214
+ messageDelayMaxMs: cfg.messageDelayMaxMs,
215
+ burstSize: cfg.burstSize,
216
+ burstPauseMinMs: cfg.burstPauseMinMs,
217
+ burstPauseMaxMs: cfg.burstPauseMaxMs,
218
+ typingSimulation: cfg.typingSimulation,
219
+ dailySendLimit: cfg.dailySendLimit,
220
+ checkRecipientExists: cfg.checkRecipientExists,
221
+ };
222
+ socketConfig = antiBanCfg;
223
+ sessionPath = cfg.sessionPath;
224
+ queue = new p_queue_1.default({ concurrency: 1 });
225
+ // Start QR server for first-time setup
226
+ const resolvedPath = cfg.sessionPath.startsWith('~')
227
+ ? node_path_1.default.join(process.env.HOME || process.env.USERPROFILE || '', cfg.sessionPath.slice(1))
228
+ : cfg.sessionPath;
229
+ const hasSession = node_fs_1.default.existsSync(resolvedPath) && node_fs_1.default.readdirSync(resolvedPath).length > 0;
230
+ if (!hasSession) {
231
+ await startQrServer(cfg.qrPort);
232
+ }
233
+ const sock = await initSocket(antiBanCfg, cfg.sessionPath);
234
+ // If no session, wait for QR scan
235
+ if (!hasSession) {
236
+ const qrData = await new Promise((resolve) => {
237
+ qrResolve = resolve;
238
+ // Timeout after 5 minutes
239
+ setTimeout(() => {
240
+ if (qrResolve) {
241
+ qrResolve(null);
242
+ qrResolve = null;
243
+ }
244
+ }, 300000);
245
+ });
246
+ if (!qrData) {
247
+ throw new n8n_workflow_1.NodeApiError({}, {
248
+ message: 'QR code expired. Please try again.',
249
+ });
250
+ }
251
+ // Wait for connection to open after scan
252
+ await new Promise((resolve, reject) => {
253
+ const timeout = setTimeout(() => reject(new Error('Connection timeout after QR scan')), 60000);
254
+ const checkInterval = setInterval(() => {
255
+ if (socketStatus === 'connected') {
256
+ clearTimeout(timeout);
257
+ clearInterval(checkInterval);
258
+ resolve();
259
+ }
260
+ if (socketStatus === 'logged_out' || socketStatus === 'error') {
261
+ clearTimeout(timeout);
262
+ clearInterval(checkInterval);
263
+ reject(new Error('Connection failed after QR scan'));
264
+ }
265
+ }, 1000);
266
+ });
267
+ }
268
+ return sock;
269
+ }
270
+ async function simulateTyping(jid, content, cfg) {
271
+ if (!cfg.typingSimulation || !socketInstance)
272
+ return;
273
+ try {
274
+ await socketInstance.sendPresenceUpdate('composing', jid);
275
+ const len = (content.text || content.caption || '').length;
276
+ await sleep(Math.min(randBetween(900, 1800) + len * 25, 4000));
277
+ await socketInstance.sendPresenceUpdate('paused', jid);
278
+ }
279
+ catch {
280
+ // presence is best-effort
281
+ }
282
+ }
283
+ async function sendMessageWithAntiBan(to, content, cfg) {
284
+ const sock = await ensureConnected(cfg);
285
+ // Daily limit check
286
+ if (cfg.dailySendLimit > 0) {
287
+ const today = todayStartIso();
288
+ if (sentTodayDate !== today) {
289
+ sentTodayDate = today;
290
+ sentTodayCount = 0;
291
+ }
292
+ if (sentTodayCount >= cfg.dailySendLimit) {
293
+ throw new n8n_workflow_1.NodeApiError({}, {
294
+ message: `Daily send limit (${cfg.dailySendLimit}) reached. Resets at midnight UTC.`,
295
+ });
296
+ }
297
+ }
298
+ const jid = normalizeRecipient(to);
299
+ // Recipient validation
300
+ if (cfg.checkRecipientExists && !jid.endsWith('@g.us')) {
301
+ const results = await sock.onWhatsApp(jid);
302
+ const result = results === null || results === void 0 ? void 0 : results[0];
303
+ if (!(result === null || result === void 0 ? void 0 : result.exists)) {
304
+ throw new n8n_workflow_1.NodeApiError({}, {
305
+ message: `Recipient ${to} is not registered on WhatsApp.`,
306
+ });
307
+ }
308
+ }
309
+ // Queue the send through anti-ban
310
+ const antiBanCfg = {
311
+ messageDelayMinMs: cfg.messageDelayMinMs,
312
+ messageDelayMaxMs: cfg.messageDelayMaxMs,
313
+ burstSize: cfg.burstSize,
314
+ burstPauseMinMs: cfg.burstPauseMinMs,
315
+ burstPauseMaxMs: cfg.burstPauseMaxMs,
316
+ typingSimulation: cfg.typingSimulation,
317
+ dailySendLimit: cfg.dailySendLimit,
318
+ checkRecipientExists: cfg.checkRecipientExists,
319
+ };
320
+ const result = await queue.add(async () => {
321
+ var _a;
322
+ const wait = nextSendAt - Date.now();
323
+ if (wait > 0)
324
+ await sleep(wait);
325
+ // Typing simulation
326
+ await simulateTyping(jid, content, antiBanCfg);
327
+ // Build message content
328
+ const msgContent = {};
329
+ if (content.text)
330
+ msgContent.text = content.text;
331
+ if (content.image)
332
+ msgContent.image = content.image;
333
+ if (content.document)
334
+ msgContent.document = content.document;
335
+ if (content.audio)
336
+ msgContent.audio = content.audio;
337
+ if (content.location)
338
+ msgContent.location = content.location;
339
+ if (content.mimetype)
340
+ msgContent.mimetype = content.mimetype;
341
+ if (content.fileName)
342
+ msgContent.fileName = content.fileName;
343
+ if (content.caption)
344
+ msgContent.caption = content.caption;
345
+ if (content.ptt !== undefined)
346
+ msgContent.ptt = content.ptt;
347
+ const response = await sock.sendMessage(jid, msgContent);
348
+ // Update anti-ban timing
349
+ nextSendAt = Date.now() + sendGapMs(antiBanCfg) + burstPauseMs(antiBanCfg);
350
+ // Update daily count
351
+ if (cfg.dailySendLimit > 0)
352
+ sentTodayCount++;
353
+ if (!((_a = response === null || response === void 0 ? void 0 : response.key) === null || _a === void 0 ? void 0 : _a.id)) {
354
+ throw new Error('WhatsApp did not return a message ID');
355
+ }
356
+ return {
357
+ messageId: response.key.id,
358
+ status: 'sent',
359
+ recipient: jid,
360
+ };
361
+ });
362
+ return result;
363
+ }
364
+ function getConnectionStatus() {
365
+ var _a;
366
+ return {
367
+ status: socketStatus,
368
+ connected: socketStatus === 'connected',
369
+ qrAvailable: socketStatus === 'qr_ready',
370
+ queueSize: queue ? queue.size + queue.pending : 0,
371
+ sentToday: sentTodayCount,
372
+ dailyLimit: (_a = socketConfig === null || socketConfig === void 0 ? void 0 : socketConfig.dailySendLimit) !== null && _a !== void 0 ? _a : 0,
373
+ };
374
+ }
375
+ function parseIncomingMessage(msg) {
376
+ var _a, _b, _c, _d, _e, _f, _g;
377
+ if (!msg.message || !msg.key || !msg.key.remoteJid)
378
+ return null;
379
+ let content = null;
380
+ const messageType = Object.keys(msg.message)[0];
381
+ if (msg.message.conversation) {
382
+ content = msg.message.conversation;
383
+ }
384
+ else if ((_a = msg.message.extendedTextMessage) === null || _a === void 0 ? void 0 : _a.text) {
385
+ content = msg.message.extendedTextMessage.text;
386
+ }
387
+ else if ((_b = msg.message.imageMessage) === null || _b === void 0 ? void 0 : _b.caption) {
388
+ content = msg.message.imageMessage.caption;
389
+ }
390
+ else if ((_c = msg.message.videoMessage) === null || _c === void 0 ? void 0 : _c.caption) {
391
+ content = msg.message.videoMessage.caption;
392
+ }
393
+ else if ((_d = msg.message.documentMessage) === null || _d === void 0 ? void 0 : _d.fileName) {
394
+ content = msg.message.documentMessage.fileName;
395
+ }
396
+ else if (msg.message.audioMessage) {
397
+ content = '[Audio]';
398
+ }
399
+ else if (msg.message.stickerMessage) {
400
+ content = '[Sticker]';
401
+ }
402
+ else if (msg.message.locationMessage) {
403
+ content = `[Location] ${msg.message.locationMessage.address || ''}`;
404
+ }
405
+ else if ((_e = msg.message.contactMessage) === null || _e === void 0 ? void 0 : _e.displayName) {
406
+ content = `[Contact] ${msg.message.contactMessage.displayName}`;
407
+ }
408
+ else if ((_f = msg.message.pollCreationMessage) === null || _f === void 0 ? void 0 : _f.name) {
409
+ content = `[Poll] ${msg.message.pollCreationMessage.name}`;
410
+ }
411
+ const timestampSeconds = msg.messageTimestamp != null
412
+ ? Number(msg.messageTimestamp)
413
+ : Date.now() / 1000;
414
+ return {
415
+ messageId: msg.key.id,
416
+ chatJid: msg.key.remoteJid,
417
+ sender: msg.key.participant || (msg.key.remoteJid !== msg.key.participant ? msg.key.remoteJid : null),
418
+ content: content || `[${messageType}]`,
419
+ timestamp: new Date(timestampSeconds * 1000).toISOString(),
420
+ isFromMe: (_g = msg.key.fromMe) !== null && _g !== void 0 ? _g : false,
421
+ messageType,
422
+ };
423
+ }
@@ -0,0 +1,5 @@
1
+ import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class WhatsAppSend implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WhatsAppSend = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const WhatsAppApiHelper_1 = require("./WhatsAppApiHelper");
6
+ class WhatsAppSend {
7
+ constructor() {
8
+ this.description = {
9
+ displayName: 'WhatsApp Send',
10
+ name: 'whatsAppSend',
11
+ icon: 'file:icons/whatsapp.svg',
12
+ group: ['transform'],
13
+ version: 1,
14
+ subtitle: '={{$parameter["operation"]}}',
15
+ description: 'Send WhatsApp messages with anti-ban protection',
16
+ defaults: { name: 'WhatsApp Send' },
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: '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' },
32
+ ],
33
+ default: 'sendText',
34
+ },
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 (e.g., 1234567890@s.whatsapp.net)',
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 for the image or document',
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: 'MIME Type',
80
+ name: 'mimeType',
81
+ type: 'string',
82
+ default: '',
83
+ displayOptions: { show: { operation: ['sendImage', 'sendDocument', 'sendAudio'] } },
84
+ description: 'MIME type (auto-detected if empty)',
85
+ },
86
+ {
87
+ displayName: 'Voice Note',
88
+ name: 'ptt',
89
+ type: 'boolean',
90
+ default: false,
91
+ displayOptions: { show: { operation: ['sendAudio'] } },
92
+ description: 'Send as voice note (push-to-talk) instead of audio file',
93
+ },
94
+ {
95
+ displayName: 'Latitude',
96
+ name: 'latitude',
97
+ type: 'number',
98
+ default: 0,
99
+ required: true,
100
+ displayOptions: { show: { operation: ['sendLocation'] } },
101
+ description: 'Location latitude',
102
+ },
103
+ {
104
+ displayName: 'Longitude',
105
+ name: 'longitude',
106
+ type: 'number',
107
+ default: 0,
108
+ required: true,
109
+ displayOptions: { show: { operation: ['sendLocation'] } },
110
+ description: 'Location longitude',
111
+ },
112
+ {
113
+ displayName: 'Location Name',
114
+ name: 'locationName',
115
+ type: 'string',
116
+ default: '',
117
+ displayOptions: { show: { operation: ['sendLocation'] } },
118
+ description: 'Optional name for the location',
119
+ },
120
+ {
121
+ displayName: 'Location Address',
122
+ name: 'locationAddress',
123
+ type: 'string',
124
+ default: '',
125
+ displayOptions: { show: { operation: ['sendLocation'] } },
126
+ description: 'Optional address for the location',
127
+ },
128
+ ],
129
+ };
130
+ }
131
+ async execute() {
132
+ const items = this.getInputData();
133
+ const returnData = [];
134
+ const credentials = await this.getCredentials('whatsappApi');
135
+ const cfg = await (0, WhatsAppApiHelper_1.getWhatsAppCredentials)(credentials);
136
+ for (let i = 0; i < items.length; i++) {
137
+ try {
138
+ const operation = this.getNodeParameter('operation', i);
139
+ const recipient = this.getNodeParameter('recipient', i);
140
+ let content = {};
141
+ switch (operation) {
142
+ case 'sendText':
143
+ content = { text: this.getNodeParameter('text', i) };
144
+ break;
145
+ case 'sendImage': {
146
+ const binaryProperty = this.getNodeParameter('binaryProperty', i);
147
+ const buffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty);
148
+ content = {
149
+ image: buffer,
150
+ caption: this.getNodeParameter('caption', i) || undefined,
151
+ mimeType: this.getNodeParameter('mimeType', i) || undefined,
152
+ };
153
+ break;
154
+ }
155
+ case 'sendDocument': {
156
+ const binaryProperty = this.getNodeParameter('binaryProperty', i);
157
+ const buffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty);
158
+ content = {
159
+ document: buffer,
160
+ fileName: this.getNodeParameter('fileName', i),
161
+ caption: this.getNodeParameter('caption', i) || undefined,
162
+ mimeType: this.getNodeParameter('mimeType', i) || undefined,
163
+ };
164
+ break;
165
+ }
166
+ case 'sendAudio': {
167
+ const binaryProperty = this.getNodeParameter('binaryProperty', i);
168
+ const buffer = await this.helpers.getBinaryDataBuffer(i, binaryProperty);
169
+ content = {
170
+ audio: buffer,
171
+ ptt: this.getNodeParameter('ptt', i),
172
+ mimeType: this.getNodeParameter('mimeType', i) || undefined,
173
+ };
174
+ break;
175
+ }
176
+ case 'sendLocation':
177
+ content = {
178
+ location: {
179
+ degreesLatitude: this.getNodeParameter('latitude', i),
180
+ degreesLongitude: this.getNodeParameter('longitude', i),
181
+ name: this.getNodeParameter('locationName', i) || undefined,
182
+ address: this.getNodeParameter('locationAddress', i) || undefined,
183
+ },
184
+ };
185
+ break;
186
+ }
187
+ const result = await (0, WhatsAppApiHelper_1.sendMessageWithAntiBan)(recipient, content, cfg);
188
+ returnData.push({
189
+ json: {
190
+ ...result,
191
+ operation,
192
+ success: true,
193
+ },
194
+ pairedItem: { item: i },
195
+ });
196
+ }
197
+ catch (error) {
198
+ if (this.continueOnFail()) {
199
+ returnData.push({
200
+ json: {
201
+ error: error.message,
202
+ success: false,
203
+ },
204
+ pairedItem: { item: i },
205
+ });
206
+ continue;
207
+ }
208
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), error);
209
+ }
210
+ }
211
+ return [returnData];
212
+ }
213
+ }
214
+ exports.WhatsAppSend = WhatsAppSend;
@@ -0,0 +1,5 @@
1
+ import { INodeType, INodeTypeDescription, INodeExecutionData, IPollFunctions } from 'n8n-workflow';
2
+ export declare class WhatsAppTrigger implements INodeType {
3
+ description: INodeTypeDescription;
4
+ poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null>;
5
+ }
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WhatsAppTrigger = void 0;
4
+ const WhatsAppApiHelper_1 = require("./WhatsAppApiHelper");
5
+ const messageBuffer = [];
6
+ let socketInitialized = false;
7
+ class WhatsAppTrigger {
8
+ constructor() {
9
+ this.description = {
10
+ displayName: 'WhatsApp Trigger',
11
+ name: 'whatsAppTrigger',
12
+ icon: 'file:icons/whatsapp.svg',
13
+ group: ['trigger'],
14
+ version: 1,
15
+ subtitle: '={{$parameter["event"]}}',
16
+ description: 'Triggers on incoming WhatsApp messages',
17
+ defaults: { name: 'WhatsApp Trigger' },
18
+ inputs: [],
19
+ outputs: ['main'],
20
+ credentials: [{ name: 'whatsappApi', required: true }],
21
+ polling: true,
22
+ properties: [
23
+ {
24
+ displayName: 'Event',
25
+ name: 'event',
26
+ type: 'options',
27
+ options: [
28
+ { name: 'Message Received', value: 'messageReceived', description: 'Trigger when any message is received' },
29
+ ],
30
+ default: 'messageReceived',
31
+ },
32
+ {
33
+ displayName: 'Chat JID Filter',
34
+ name: 'chatJidFilter',
35
+ type: 'string',
36
+ default: '',
37
+ description: 'Optional: only trigger for messages from this chat JID (e.g., 1234567890@s.whatsapp.net). Leave empty for all chats.',
38
+ },
39
+ {
40
+ displayName: 'Only Text Messages',
41
+ name: 'onlyText',
42
+ type: 'boolean',
43
+ default: false,
44
+ description: 'If true, only trigger for text messages (ignores images, documents, etc.)',
45
+ },
46
+ ],
47
+ };
48
+ }
49
+ async poll() {
50
+ const credentials = await this.getCredentials('whatsappApi');
51
+ const cfg = await (0, WhatsAppApiHelper_1.getWhatsAppCredentials)(credentials);
52
+ const chatFilter = this.getNodeParameter('chatJidFilter', '');
53
+ const onlyText = this.getNodeParameter('onlyText', false);
54
+ // Initialize socket listener once
55
+ if (!socketInitialized) {
56
+ const sock = await (0, WhatsAppApiHelper_1.ensureConnected)(cfg);
57
+ sock.ev.on('messages.upsert', (upsert) => {
58
+ const { messages, type } = upsert;
59
+ if (type !== 'notify')
60
+ return;
61
+ for (const msg of messages) {
62
+ if (msg.key.fromMe)
63
+ continue;
64
+ if (chatFilter && msg.key.remoteJid !== chatFilter)
65
+ continue;
66
+ const parsed = (0, WhatsAppApiHelper_1.parseIncomingMessage)(msg);
67
+ if (!parsed)
68
+ continue;
69
+ if (onlyText && parsed.messageType !== 'conversation' && parsed.messageType !== 'extendedTextMessage')
70
+ continue;
71
+ messageBuffer.push(parsed);
72
+ }
73
+ });
74
+ socketInitialized = true;
75
+ }
76
+ // Return buffered messages
77
+ if (messageBuffer.length === 0)
78
+ return null;
79
+ const messages = messageBuffer.splice(0, messageBuffer.length);
80
+ return [messages.map((msg) => ({
81
+ json: msg,
82
+ pairedItem: { item: 0 },
83
+ }))];
84
+ }
85
+ }
86
+ exports.WhatsAppTrigger = WhatsAppTrigger;
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#25D366"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/></svg>
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@raevon/n8n-nodes-whatsapp",
3
+ "version": "1.0.0",
4
+ "description": "n8n community node for WhatsApp — send and receive messages with anti-ban protection via the Baileys library",
5
+ "keywords": [
6
+ "n8n-community-node-package",
7
+ "whatsapp",
8
+ "messaging",
9
+ "automation"
10
+ ],
11
+ "license": "MIT",
12
+ "author": {
13
+ "name": "n8n-nodes-whatsapp"
14
+ },
15
+ "main": "index.js",
16
+ "scripts": {
17
+ "build": "tsc && mkdir -p dist/nodes/WhatsApp/icons && cp nodes/WhatsApp/icons/whatsapp.svg dist/nodes/WhatsApp/icons/whatsapp.svg",
18
+ "dev": "tsc --watch",
19
+ "lint": "eslint credentials nodes --ext .ts",
20
+ "lintfix": "eslint credentials nodes --ext .ts --fix",
21
+ "prepublishOnly": "npm run build"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "n8n": {
27
+ "n8nNodesApiVersion": 1,
28
+ "credentials": [
29
+ "dist/credentials/WhatsAppApi.credentials.js"
30
+ ],
31
+ "nodes": [
32
+ "dist/nodes/WhatsApp/WhatsAppSend.node.js",
33
+ "dist/nodes/WhatsApp/WhatsAppTrigger.node.js"
34
+ ]
35
+ },
36
+ "dependencies": {
37
+ "@whiskeysockets/baileys": "^7.0.0-rc13",
38
+ "p-queue": "^8.0.1",
39
+ "pino": "^9.6.0",
40
+ "open": "^10.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^20.19.39",
44
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
45
+ "@typescript-eslint/parser": "^6.21.0",
46
+ "eslint": "^8.57.1",
47
+ "n8n-workflow": "^2.16.0",
48
+ "typescript": "^5.9.3"
49
+ },
50
+ "peerDependencies": {
51
+ "n8n-workflow": ">=1.0.0"
52
+ }
53
+ }