@raevon/n8n-nodes-whatsapp 1.0.14 → 2.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.
- package/dist/nodes/WhatsApp/WhatsAppApiHelper.d.ts +3 -52
- package/dist/nodes/WhatsApp/WhatsAppApiHelper.js +76 -468
- package/dist/nodes/WhatsApp/WhatsAppConnect.node.js +30 -32
- package/dist/nodes/WhatsApp/WhatsAppSend.node.js +30 -70
- package/dist/nodes/WhatsApp/WhatsAppServer.d.ts +1 -0
- package/dist/nodes/WhatsApp/WhatsAppServer.js +390 -0
- package/dist/nodes/WhatsApp/WhatsAppTrigger.node.js +27 -41
- package/package.json +1 -1
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { type WASocket } from '@whiskeysockets/baileys';
|
|
2
1
|
export interface WhatsAppCredentials {
|
|
3
2
|
sessionPath: string;
|
|
4
3
|
messageDelayMinMs: number;
|
|
@@ -10,56 +9,8 @@ export interface WhatsAppCredentials {
|
|
|
10
9
|
dailySendLimit: number;
|
|
11
10
|
checkRecipientExists: boolean;
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
messageDelayMaxMs: number;
|
|
16
|
-
burstSize: number;
|
|
17
|
-
burstPauseMinMs: number;
|
|
18
|
-
burstPauseMaxMs: number;
|
|
19
|
-
typingSimulation: boolean;
|
|
20
|
-
dailySendLimit: number;
|
|
21
|
-
checkRecipientExists: boolean;
|
|
22
|
-
}
|
|
23
|
-
export declare function getWhatsAppCredentials(credentials: Record<string, any>): Promise<WhatsAppCredentials>;
|
|
24
|
-
export declare function ensureConnected(cfg: WhatsAppCredentials): Promise<WASocket>;
|
|
25
|
-
export declare function connectOrGetQr(cfg: WhatsAppCredentials): Promise<{
|
|
26
|
-
connected: boolean;
|
|
27
|
-
qrUrl?: string;
|
|
28
|
-
message: string;
|
|
29
|
-
}>;
|
|
30
|
-
export declare function simulateTyping(jid: string, content: {
|
|
31
|
-
text?: string;
|
|
32
|
-
caption?: string;
|
|
33
|
-
}, cfg: AntiBanConfig): Promise<void>;
|
|
34
|
-
export declare function sendMessageWithAntiBan(to: string, content: {
|
|
35
|
-
text?: string;
|
|
36
|
-
image?: Buffer;
|
|
37
|
-
document?: Buffer;
|
|
38
|
-
audio?: Buffer;
|
|
39
|
-
location?: {
|
|
40
|
-
degreesLatitude: number;
|
|
41
|
-
degreesLongitude: number;
|
|
42
|
-
name?: string;
|
|
43
|
-
address?: string;
|
|
44
|
-
};
|
|
45
|
-
mimetype?: string;
|
|
46
|
-
fileName?: string;
|
|
47
|
-
caption?: string;
|
|
48
|
-
ptt?: boolean;
|
|
49
|
-
}, cfg: WhatsAppCredentials): Promise<{
|
|
50
|
-
messageId: string;
|
|
51
|
-
status: string;
|
|
52
|
-
recipient: string;
|
|
53
|
-
}>;
|
|
54
|
-
export declare function getConnectionStatus(): {
|
|
55
|
-
status: string;
|
|
56
|
-
connected: boolean;
|
|
57
|
-
qrAvailable: boolean;
|
|
58
|
-
queueSize: number;
|
|
59
|
-
sentToday: number;
|
|
60
|
-
dailyLimit: number;
|
|
61
|
-
lastError: string | null;
|
|
62
|
-
};
|
|
12
|
+
export declare function getServerUrl(cfg: WhatsAppCredentials): string;
|
|
13
|
+
export declare function ensureServerRunning(cfg: WhatsAppCredentials): Promise<void>;
|
|
63
14
|
export declare function disconnect(): Promise<void>;
|
|
64
15
|
export declare function parseIncomingMessage(msg: any): Record<string, any> | null;
|
|
65
|
-
export
|
|
16
|
+
export declare function getWhatsAppCredentials(credentials: Record<string, any>): Promise<WhatsAppCredentials>;
|
|
@@ -1,504 +1,99 @@
|
|
|
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
|
-
})();
|
|
35
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
4
|
};
|
|
38
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.
|
|
40
|
-
exports.
|
|
41
|
-
exports.connectOrGetQr = connectOrGetQr;
|
|
42
|
-
exports.simulateTyping = simulateTyping;
|
|
43
|
-
exports.sendMessageWithAntiBan = sendMessageWithAntiBan;
|
|
44
|
-
exports.getConnectionStatus = getConnectionStatus;
|
|
6
|
+
exports.getServerUrl = getServerUrl;
|
|
7
|
+
exports.ensureServerRunning = ensureServerRunning;
|
|
45
8
|
exports.disconnect = disconnect;
|
|
46
9
|
exports.parseIncomingMessage = parseIncomingMessage;
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const p_queue_1 = __importDefault(require("p-queue"));
|
|
10
|
+
exports.getWhatsAppCredentials = getWhatsAppCredentials;
|
|
11
|
+
const node_child_process_1 = require("node:child_process");
|
|
50
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
51
13
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
52
|
-
const
|
|
53
|
-
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
54
|
-
const randBetween = (min, max) => max > min ? min + Math.floor(Math.random() * (max - min + 1)) : min;
|
|
55
|
-
// Silent logger — Baileys expects pino-compatible logger with these methods
|
|
56
|
-
const noop = () => { };
|
|
57
|
-
const silentLogger = {
|
|
58
|
-
level: 'silent',
|
|
59
|
-
info: noop,
|
|
60
|
-
warn: noop,
|
|
61
|
-
error: noop,
|
|
62
|
-
debug: noop,
|
|
63
|
-
fatal: noop,
|
|
64
|
-
trace: noop,
|
|
65
|
-
child: () => silentLogger,
|
|
66
|
-
};
|
|
67
|
-
// I8: Extracted ~ path expansion to avoid duplication
|
|
14
|
+
const node_net_1 = __importDefault(require("node:net"));
|
|
68
15
|
function expandHome(p) {
|
|
69
16
|
if (!p.startsWith('~'))
|
|
70
17
|
return p;
|
|
71
18
|
return node_path_1.default.join(process.env.HOME || process.env.USERPROFILE || '', p.slice(1));
|
|
72
19
|
}
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// single-credential setup. Multi-credential support would require a Map<sessionPath, State>.
|
|
78
|
-
let socketInstance = null;
|
|
79
|
-
let socketStatus = 'stopped';
|
|
80
|
-
let socketConfig = null;
|
|
81
|
-
let sessionPath = null;
|
|
82
|
-
let queue = null;
|
|
83
|
-
let nextSendAt = 0;
|
|
84
|
-
let sentInBurst = 0;
|
|
85
|
-
let sentTodayCount = 0;
|
|
86
|
-
let sentTodayDate = '';
|
|
87
|
-
let reconnectAttempts = 0;
|
|
88
|
-
let reconnectTimer = null;
|
|
89
|
-
let qrResolve = null;
|
|
90
|
-
let latestQr = null;
|
|
91
|
-
let generation = 0; // #10: Generation counter — prevents stale handlers on reconnect
|
|
92
|
-
let credsSavedPromise = null; // Track credential save completion
|
|
93
|
-
let lastDisconnectError = null; // Track last disconnect reason for output
|
|
94
|
-
function todayStartIso() {
|
|
95
|
-
return new Date().toISOString().slice(0, 10) + 'T00:00:00.000Z';
|
|
20
|
+
function getPort(cfg) {
|
|
21
|
+
// Derive port from session path to avoid conflicts between credentials
|
|
22
|
+
const hash = cfg.sessionPath.split('').reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0);
|
|
23
|
+
return 3456 + (Math.abs(hash) % 1000);
|
|
96
24
|
}
|
|
97
|
-
function
|
|
98
|
-
return
|
|
25
|
+
function getServerUrl(cfg) {
|
|
26
|
+
return `http://127.0.0.1:${getPort(cfg)}`;
|
|
99
27
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
function normalizeRecipient(to) {
|
|
108
|
-
if (to.endsWith('@s.whatsapp.net') || to.endsWith('@g.us'))
|
|
109
|
-
return to;
|
|
110
|
-
const digits = to.replace(/\D/g, '');
|
|
111
|
-
if (!/^[1-9][0-9]{7,14}$/.test(digits)) {
|
|
112
|
-
throw new Error(`Invalid phone number: ${to}`);
|
|
113
|
-
}
|
|
114
|
-
return `${digits}@s.whatsapp.net`;
|
|
28
|
+
async function isPortOpen(port) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const server = node_net_1.default.createServer();
|
|
31
|
+
server.once('error', () => resolve(false));
|
|
32
|
+
server.once('listening', () => { server.close(); resolve(true); });
|
|
33
|
+
server.listen(port, '127.0.0.1');
|
|
34
|
+
});
|
|
115
35
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
36
|
+
let serverProcess = null;
|
|
37
|
+
let serverPort = 0;
|
|
38
|
+
async function ensureServerRunning(cfg) {
|
|
39
|
+
const port = getPort(cfg);
|
|
40
|
+
// Check if server is already running
|
|
41
|
+
if (await isPortOpen(port)) {
|
|
42
|
+
serverPort = port;
|
|
119
43
|
return;
|
|
120
|
-
const base = Math.min(1000 * (2 ** reconnectAttempts), 60000);
|
|
121
|
-
const delay = Math.round(base * (0.8 + Math.random() * 0.4));
|
|
122
|
-
reconnectAttempts += 1;
|
|
123
|
-
reconnectTimer = setTimeout(() => {
|
|
124
|
-
reconnectTimer = null;
|
|
125
|
-
initSocket(cfg, sessionPath).catch(() => { });
|
|
126
|
-
}, delay);
|
|
127
|
-
}
|
|
128
|
-
async function initSocket(cfg, authPath) {
|
|
129
|
-
// Always create fresh socket if current one is dead
|
|
130
|
-
if (socketInstance && socketStatus === 'connected') {
|
|
131
|
-
try {
|
|
132
|
-
if (socketInstance.user)
|
|
133
|
-
return socketInstance;
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
// Socket dead, create new one
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
const gen = ++generation; // #10: Snapshot generation for this connection attempt
|
|
140
|
-
const resolvedPath = expandHome(authPath);
|
|
141
|
-
if (!node_fs_1.default.existsSync(resolvedPath)) {
|
|
142
|
-
node_fs_1.default.mkdirSync(resolvedPath, { recursive: true });
|
|
143
44
|
}
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
socketStatus = 'connecting';
|
|
163
|
-
reconnectAttempts = 0;
|
|
164
|
-
sock.ev.on('creds.update', () => {
|
|
165
|
-
if (gen !== generation)
|
|
166
|
-
return; // #10: Ignore stale connection events
|
|
167
|
-
// Track credential save completion — ensures session is persisted before execution ends
|
|
168
|
-
credsSavedPromise = saveCreds().catch((err) => {
|
|
169
|
-
console.error('[WhatsApp] Failed to save credentials:', err.message);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
sock.ev.on('connection.update', async (update) => {
|
|
173
|
-
var _a, _b, _c;
|
|
174
|
-
if (gen !== generation)
|
|
175
|
-
return; // #10: Ignore stale connection events
|
|
176
|
-
const { connection, lastDisconnect, qr } = update;
|
|
177
|
-
if (qr) {
|
|
178
|
-
latestQr = qr;
|
|
179
|
-
socketStatus = 'qr_ready';
|
|
180
|
-
// Generate QR locally using qrcode package — no external service
|
|
181
|
-
if (qrResolve) {
|
|
182
|
-
const qrDataUrl = await qrcode_1.default.toDataURL(qr, { width: 300, margin: 2 });
|
|
183
|
-
qrResolve({ qr, qrUrl: qrDataUrl });
|
|
184
|
-
qrResolve = null;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
if (connection === 'open') {
|
|
188
|
-
socketStatus = 'connected';
|
|
189
|
-
reconnectAttempts = 0;
|
|
190
|
-
latestQr = null;
|
|
191
|
-
lastDisconnectError = null;
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (connection === 'close') {
|
|
195
|
-
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;
|
|
196
|
-
const errorMsg = ((_c = lastDisconnect === null || lastDisconnect === void 0 ? void 0 : lastDisconnect.error) === null || _c === void 0 ? void 0 : _c.message) || 'Unknown error';
|
|
197
|
-
const reason = baileys_1.DisconnectReason[code] || 'Unknown';
|
|
198
|
-
// Store error for output in nodes
|
|
199
|
-
lastDisconnectError = `Code: ${code} (${reason}), Error: ${errorMsg}`;
|
|
200
|
-
if (code === baileys_1.DisconnectReason.loggedOut) {
|
|
201
|
-
socketStatus = 'logged_out';
|
|
202
|
-
socketInstance = null;
|
|
203
|
-
++generation;
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
socketStatus = 'disconnected';
|
|
207
|
-
socketInstance = null;
|
|
208
|
-
// Don't auto-reconnect if we're in a loop — let the node handle it
|
|
209
|
-
if (reconnectAttempts < 3) {
|
|
210
|
-
scheduleReconnect(cfg);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
// #8: Fire-and-forget — no receipt tracking, no delivery acks, minimal protocol chatter
|
|
216
|
-
return sock;
|
|
217
|
-
}
|
|
218
|
-
// --- Public API ---
|
|
219
|
-
async function getWhatsAppCredentials(credentials) {
|
|
220
|
-
var _a, _b;
|
|
221
|
-
return {
|
|
222
|
-
sessionPath: credentials.sessionPath || '~/.n8n/whatsapp-auth',
|
|
223
|
-
messageDelayMinMs: credentials.messageDelayMinMs || 5000,
|
|
224
|
-
messageDelayMaxMs: credentials.messageDelayMaxMs || 9000,
|
|
225
|
-
burstSize: (_a = credentials.burstSize) !== null && _a !== void 0 ? _a : 20,
|
|
226
|
-
burstPauseMinMs: credentials.burstPauseMinMs || 30000,
|
|
227
|
-
burstPauseMaxMs: credentials.burstPauseMaxMs || 60000,
|
|
228
|
-
typingSimulation: credentials.typingSimulation !== false,
|
|
229
|
-
dailySendLimit: (_b = credentials.dailySendLimit) !== null && _b !== void 0 ? _b : 500,
|
|
230
|
-
checkRecipientExists: credentials.checkRecipientExists !== false,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
async function ensureConnected(cfg) {
|
|
234
|
-
const antiBanCfg = {
|
|
235
|
-
messageDelayMinMs: cfg.messageDelayMinMs,
|
|
236
|
-
messageDelayMaxMs: cfg.messageDelayMaxMs,
|
|
237
|
-
burstSize: cfg.burstSize,
|
|
238
|
-
burstPauseMinMs: cfg.burstPauseMinMs,
|
|
239
|
-
burstPauseMaxMs: cfg.burstPauseMaxMs,
|
|
240
|
-
typingSimulation: cfg.typingSimulation,
|
|
241
|
-
dailySendLimit: cfg.dailySendLimit,
|
|
242
|
-
checkRecipientExists: cfg.checkRecipientExists,
|
|
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
|
+
// Start the server as background process
|
|
51
|
+
const env = {
|
|
52
|
+
...process.env,
|
|
53
|
+
WHATSAPP_SESSION_PATH: cfg.sessionPath,
|
|
54
|
+
WHATSAPP_PORT: String(port),
|
|
55
|
+
MESSAGE_DELAY_MIN: String(cfg.messageDelayMinMs),
|
|
56
|
+
MESSAGE_DELAY_MAX: String(cfg.messageDelayMaxMs),
|
|
57
|
+
BURST_SIZE: String(cfg.burstSize),
|
|
58
|
+
BURST_PAUSE_MIN: String(cfg.burstPauseMinMs),
|
|
59
|
+
BURST_PAUSE_MAX: String(cfg.burstPauseMaxMs),
|
|
60
|
+
TYPING_SIMULATION: String(cfg.typingSimulation),
|
|
61
|
+
DAILY_SEND_LIMIT: String(cfg.dailySendLimit),
|
|
62
|
+
CHECK_RECIPIENT: String(cfg.checkRecipientExists),
|
|
243
63
|
};
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
// Check if current socket is alive — reuse if possible
|
|
259
|
-
if (socketInstance && socketStatus === 'connected') {
|
|
260
|
-
try {
|
|
261
|
-
// Test if socket is actually responding
|
|
262
|
-
await socketInstance.query({ tag: 'iq', attrs: { id: 'ping', to: 's.whatsapp.net', type: 'get', xmlns: 'w:p' } });
|
|
263
|
-
return socketInstance;
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
// Socket is dead, create new one
|
|
267
|
-
socketInstance = null;
|
|
268
|
-
socketStatus = 'stopped';
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
// Create fresh connection from saved session
|
|
272
|
-
console.log('[WhatsApp] Creating fresh connection from saved session...');
|
|
273
|
-
const sock = await initSocket(antiBanCfg, cfg.sessionPath);
|
|
274
|
-
// Wait for connection to open (up to 20s)
|
|
275
|
-
const connected = await new Promise((resolve) => {
|
|
276
|
-
const check = setInterval(() => {
|
|
277
|
-
if (socketStatus === 'connected') {
|
|
64
|
+
serverProcess = (0, node_child_process_1.spawn)('node', [serverPath], {
|
|
65
|
+
env,
|
|
66
|
+
stdio: 'ignore',
|
|
67
|
+
detached: true,
|
|
68
|
+
});
|
|
69
|
+
serverProcess.unref();
|
|
70
|
+
serverPort = port;
|
|
71
|
+
// Wait for server to start (up to 10s)
|
|
72
|
+
const started = await new Promise((resolve) => {
|
|
73
|
+
const start = Date.now();
|
|
74
|
+
const check = setInterval(async () => {
|
|
75
|
+
if (await isPortOpen(port)) {
|
|
278
76
|
clearInterval(check);
|
|
279
77
|
resolve(true);
|
|
280
78
|
}
|
|
281
|
-
if (
|
|
79
|
+
if (Date.now() - start > 10000) {
|
|
282
80
|
clearInterval(check);
|
|
283
81
|
resolve(false);
|
|
284
82
|
}
|
|
285
83
|
}, 500);
|
|
286
|
-
setTimeout(() => { clearInterval(check); resolve(false); }, 20000);
|
|
287
84
|
});
|
|
288
|
-
if (!
|
|
289
|
-
throw new
|
|
290
|
-
message: `WhatsApp connection failed (status: ${socketStatus}). Try running Connect node again.`,
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
// Wait for credentials to be saved
|
|
294
|
-
if (credsSavedPromise) {
|
|
295
|
-
await credsSavedPromise;
|
|
296
|
-
credsSavedPromise = null;
|
|
85
|
+
if (!started) {
|
|
86
|
+
throw new Error('WhatsApp server failed to start within 10 seconds');
|
|
297
87
|
}
|
|
298
|
-
return sock;
|
|
299
88
|
}
|
|
300
|
-
// Non-blocking version for Connect node — starts socket, returns QR URL if needed, doesn't wait
|
|
301
|
-
async function connectOrGetQr(cfg) {
|
|
302
|
-
if (socketStatus === 'connected' && socketInstance) {
|
|
303
|
-
return { connected: true, message: 'Already connected' };
|
|
304
|
-
}
|
|
305
|
-
const antiBanCfg = {
|
|
306
|
-
messageDelayMinMs: cfg.messageDelayMinMs,
|
|
307
|
-
messageDelayMaxMs: cfg.messageDelayMaxMs,
|
|
308
|
-
burstSize: cfg.burstSize,
|
|
309
|
-
burstPauseMinMs: cfg.burstPauseMinMs,
|
|
310
|
-
burstPauseMaxMs: cfg.burstPauseMaxMs,
|
|
311
|
-
typingSimulation: cfg.typingSimulation,
|
|
312
|
-
dailySendLimit: cfg.dailySendLimit,
|
|
313
|
-
checkRecipientExists: cfg.checkRecipientExists,
|
|
314
|
-
};
|
|
315
|
-
socketConfig = antiBanCfg;
|
|
316
|
-
sessionPath = cfg.sessionPath;
|
|
317
|
-
if (!queue) {
|
|
318
|
-
queue = new p_queue_1.default({ concurrency: 1 });
|
|
319
|
-
}
|
|
320
|
-
const resolvedPath = expandHome(cfg.sessionPath);
|
|
321
|
-
const hasSession = node_fs_1.default.existsSync(resolvedPath) && node_fs_1.default.readdirSync(resolvedPath).length > 0;
|
|
322
|
-
// If session exists, just connect (no QR needed)
|
|
323
|
-
if (hasSession) {
|
|
324
|
-
await initSocket(antiBanCfg, cfg.sessionPath);
|
|
325
|
-
// Wait up to 15s for connection
|
|
326
|
-
await new Promise((resolve) => {
|
|
327
|
-
const check = setInterval(() => {
|
|
328
|
-
if (socketStatus === 'connected' || socketStatus === 'error' || socketStatus === 'logged_out') {
|
|
329
|
-
clearInterval(check);
|
|
330
|
-
resolve();
|
|
331
|
-
}
|
|
332
|
-
}, 500);
|
|
333
|
-
setTimeout(() => { clearInterval(check); resolve(); }, 15000);
|
|
334
|
-
});
|
|
335
|
-
// Wait for credentials to be fully saved to disk before returning
|
|
336
|
-
if (credsSavedPromise) {
|
|
337
|
-
await credsSavedPromise;
|
|
338
|
-
credsSavedPromise = null;
|
|
339
|
-
}
|
|
340
|
-
if (socketStatus === 'connected') {
|
|
341
|
-
return { connected: true, message: 'Connected successfully' };
|
|
342
|
-
}
|
|
343
|
-
return { connected: false, message: `Connection status: ${socketStatus}. Check n8n logs for details.` };
|
|
344
|
-
}
|
|
345
|
-
// No session — start socket and wait for QR (up to 30s)
|
|
346
|
-
const sock = await initSocket(antiBanCfg, cfg.sessionPath);
|
|
347
|
-
const qrData = await new Promise((resolve) => {
|
|
348
|
-
qrResolve = resolve;
|
|
349
|
-
setTimeout(() => {
|
|
350
|
-
if (qrResolve) {
|
|
351
|
-
qrResolve(null);
|
|
352
|
-
qrResolve = null;
|
|
353
|
-
}
|
|
354
|
-
}, 30000);
|
|
355
|
-
});
|
|
356
|
-
if (!qrData) {
|
|
357
|
-
return { connected: false, message: 'QR code not generated yet. Try again in a moment.' };
|
|
358
|
-
}
|
|
359
|
-
// Return QR URL immediately — user opens it in browser, scans, then runs Connect again
|
|
360
|
-
return {
|
|
361
|
-
connected: false,
|
|
362
|
-
qrUrl: qrData.qrUrl,
|
|
363
|
-
message: `Open this URL in your browser to scan QR:\n${qrData.qrUrl}\n\nAfter scanning, run this node again to complete connection.`,
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
async function simulateTyping(jid, content, cfg) {
|
|
367
|
-
if (!cfg.typingSimulation || !socketInstance)
|
|
368
|
-
return;
|
|
369
|
-
try {
|
|
370
|
-
await socketInstance.sendPresenceUpdate('composing', jid);
|
|
371
|
-
const len = (content.text || content.caption || '').length;
|
|
372
|
-
await sleep(Math.min(randBetween(900, 1800) + len * 25, 4000));
|
|
373
|
-
await socketInstance.sendPresenceUpdate('paused', jid);
|
|
374
|
-
}
|
|
375
|
-
catch {
|
|
376
|
-
// presence is best-effort
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
async function sendMessageWithAntiBan(to, content, cfg) {
|
|
380
|
-
const sock = await ensureConnected(cfg);
|
|
381
|
-
// Daily limit check
|
|
382
|
-
if (cfg.dailySendLimit > 0) {
|
|
383
|
-
const today = todayStartIso();
|
|
384
|
-
if (sentTodayDate !== today) {
|
|
385
|
-
sentTodayDate = today;
|
|
386
|
-
sentTodayCount = 0;
|
|
387
|
-
}
|
|
388
|
-
if (sentTodayCount >= cfg.dailySendLimit) {
|
|
389
|
-
throw new n8n_workflow_1.NodeApiError({}, {
|
|
390
|
-
message: `Daily send limit (${cfg.dailySendLimit}) reached. Resets at midnight UTC.`,
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
const jid = normalizeRecipient(to);
|
|
395
|
-
// Recipient validation (#6)
|
|
396
|
-
if (cfg.checkRecipientExists && !jid.endsWith('@g.us')) {
|
|
397
|
-
const results = await sock.onWhatsApp(jid);
|
|
398
|
-
const result = results === null || results === void 0 ? void 0 : results[0];
|
|
399
|
-
if (!(result === null || result === void 0 ? void 0 : result.exists)) {
|
|
400
|
-
throw new n8n_workflow_1.NodeApiError({}, {
|
|
401
|
-
message: `Recipient ${to} is not registered on WhatsApp.`,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
// Queue the send through anti-ban
|
|
406
|
-
const antiBanCfg = {
|
|
407
|
-
messageDelayMinMs: cfg.messageDelayMinMs,
|
|
408
|
-
messageDelayMaxMs: cfg.messageDelayMaxMs,
|
|
409
|
-
burstSize: cfg.burstSize,
|
|
410
|
-
burstPauseMinMs: cfg.burstPauseMinMs,
|
|
411
|
-
burstPauseMaxMs: cfg.burstPauseMaxMs,
|
|
412
|
-
typingSimulation: cfg.typingSimulation,
|
|
413
|
-
dailySendLimit: cfg.dailySendLimit,
|
|
414
|
-
checkRecipientExists: cfg.checkRecipientExists,
|
|
415
|
-
};
|
|
416
|
-
// #9: HTTP timeout race — if anti-ban delays push past 15s, return queued
|
|
417
|
-
const task = queue.add(async () => {
|
|
418
|
-
var _a;
|
|
419
|
-
const wait = nextSendAt - Date.now();
|
|
420
|
-
if (wait > 0)
|
|
421
|
-
await sleep(wait);
|
|
422
|
-
// Typing simulation (#4)
|
|
423
|
-
await simulateTyping(jid, content, antiBanCfg);
|
|
424
|
-
// Build message content
|
|
425
|
-
const msgContent = {};
|
|
426
|
-
if (content.text)
|
|
427
|
-
msgContent.text = content.text;
|
|
428
|
-
if (content.image)
|
|
429
|
-
msgContent.image = content.image;
|
|
430
|
-
if (content.document)
|
|
431
|
-
msgContent.document = content.document;
|
|
432
|
-
if (content.audio)
|
|
433
|
-
msgContent.audio = content.audio;
|
|
434
|
-
if (content.location)
|
|
435
|
-
msgContent.location = content.location;
|
|
436
|
-
if (content.mimetype)
|
|
437
|
-
msgContent.mimetype = content.mimetype;
|
|
438
|
-
if (content.fileName)
|
|
439
|
-
msgContent.fileName = content.fileName;
|
|
440
|
-
if (content.caption)
|
|
441
|
-
msgContent.caption = content.caption;
|
|
442
|
-
if (content.ptt !== undefined)
|
|
443
|
-
msgContent.ptt = content.ptt;
|
|
444
|
-
const response = await sock.sendMessage(jid, msgContent);
|
|
445
|
-
// Update anti-ban timing (#2, #3)
|
|
446
|
-
nextSendAt = Date.now() + sendGapMs(antiBanCfg) + burstPauseMs(antiBanCfg);
|
|
447
|
-
// Update daily count (#5)
|
|
448
|
-
if (cfg.dailySendLimit > 0)
|
|
449
|
-
sentTodayCount++;
|
|
450
|
-
if (!((_a = response === null || response === void 0 ? void 0 : response.key) === null || _a === void 0 ? void 0 : _a.id)) {
|
|
451
|
-
throw new Error('WhatsApp did not return a message ID');
|
|
452
|
-
}
|
|
453
|
-
return {
|
|
454
|
-
messageId: response.key.id,
|
|
455
|
-
status: 'sent',
|
|
456
|
-
recipient: jid,
|
|
457
|
-
};
|
|
458
|
-
});
|
|
459
|
-
// #9: HTTP timeout race — hand off to background if anti-ban delay exceeds 15s
|
|
460
|
-
let timer;
|
|
461
|
-
const winner = await Promise.race([
|
|
462
|
-
task.then(result => ({ result }), error => ({ error })),
|
|
463
|
-
new Promise(resolve => { timer = setTimeout(() => resolve(null), 15000); }),
|
|
464
|
-
]);
|
|
465
|
-
clearTimeout(timer);
|
|
466
|
-
if (!winner) {
|
|
467
|
-
// Queued behind anti-ban delays — let it send in background
|
|
468
|
-
task.then(() => { }, () => { });
|
|
469
|
-
return { messageId: 'queued', status: 'queued', recipient: jid };
|
|
470
|
-
}
|
|
471
|
-
const w = winner;
|
|
472
|
-
if (w.error) {
|
|
473
|
-
throw w.error;
|
|
474
|
-
}
|
|
475
|
-
return w.result;
|
|
476
|
-
}
|
|
477
|
-
function getConnectionStatus() {
|
|
478
|
-
var _a;
|
|
479
|
-
return {
|
|
480
|
-
status: socketStatus,
|
|
481
|
-
connected: socketStatus === 'connected',
|
|
482
|
-
qrAvailable: socketStatus === 'qr_ready',
|
|
483
|
-
queueSize: queue ? queue.size + queue.pending : 0,
|
|
484
|
-
sentToday: sentTodayCount,
|
|
485
|
-
dailyLimit: (_a = socketConfig === null || socketConfig === void 0 ? void 0 : socketConfig.dailySendLimit) !== null && _a !== void 0 ? _a : 0,
|
|
486
|
-
lastError: lastDisconnectError,
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
// Gracefully disconnect — important for n8n queue mode where multiple workers run
|
|
490
89
|
async function disconnect() {
|
|
491
|
-
|
|
90
|
+
// Signal server to stop (it's a separate process)
|
|
91
|
+
if (serverPort) {
|
|
492
92
|
try {
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
catch {
|
|
496
|
-
// ignore
|
|
93
|
+
await fetch(`http://127.0.0.1:${serverPort}/health`);
|
|
497
94
|
}
|
|
95
|
+
catch { }
|
|
498
96
|
}
|
|
499
|
-
socketInstance = null;
|
|
500
|
-
socketStatus = 'stopped';
|
|
501
|
-
++generation; // Invalidate any pending handlers
|
|
502
97
|
}
|
|
503
98
|
function parseIncomingMessage(msg) {
|
|
504
99
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
@@ -542,7 +137,6 @@ function parseIncomingMessage(msg) {
|
|
|
542
137
|
return {
|
|
543
138
|
messageId: msg.key.id,
|
|
544
139
|
chatJid: msg.key.remoteJid,
|
|
545
|
-
// C4: Simplified sender logic — participant is sender in groups, remoteJid in DMs
|
|
546
140
|
sender: (_g = msg.key.participant) !== null && _g !== void 0 ? _g : msg.key.remoteJid,
|
|
547
141
|
content: content || `[${messageType}]`,
|
|
548
142
|
timestamp: new Date(timestampSeconds * 1000).toISOString(),
|
|
@@ -550,3 +144,17 @@ function parseIncomingMessage(msg) {
|
|
|
550
144
|
messageType,
|
|
551
145
|
};
|
|
552
146
|
}
|
|
147
|
+
async function getWhatsAppCredentials(credentials) {
|
|
148
|
+
var _a, _b;
|
|
149
|
+
return {
|
|
150
|
+
sessionPath: credentials.sessionPath || '~/.n8n/whatsapp-auth',
|
|
151
|
+
messageDelayMinMs: credentials.messageDelayMinMs || 5000,
|
|
152
|
+
messageDelayMaxMs: credentials.messageDelayMaxMs || 9000,
|
|
153
|
+
burstSize: (_a = credentials.burstSize) !== null && _a !== void 0 ? _a : 20,
|
|
154
|
+
burstPauseMinMs: credentials.burstPauseMinMs || 30000,
|
|
155
|
+
burstPauseMaxMs: credentials.burstPauseMaxMs || 60000,
|
|
156
|
+
typingSimulation: credentials.typingSimulation !== false,
|
|
157
|
+
dailySendLimit: (_b = credentials.dailySendLimit) !== null && _b !== void 0 ? _b : 500,
|
|
158
|
+
checkRecipientExists: credentials.checkRecipientExists !== false,
|
|
159
|
+
};
|
|
160
|
+
}
|