@raevon/n8n-nodes-whatsapp 1.0.14 → 2.0.1
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 +80 -465
- 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,106 @@
|
|
|
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
|
-
socketInstance = sock;
|
|
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
|
+
const env = {
|
|
51
|
+
...process.env,
|
|
52
|
+
WHATSAPP_SESSION_PATH: cfg.sessionPath,
|
|
53
|
+
WHATSAPP_PORT: String(port),
|
|
54
|
+
MESSAGE_DELAY_MIN: String(cfg.messageDelayMinMs),
|
|
55
|
+
MESSAGE_DELAY_MAX: String(cfg.messageDelayMaxMs),
|
|
56
|
+
BURST_SIZE: String(cfg.burstSize),
|
|
57
|
+
BURST_PAUSE_MIN: String(cfg.burstPauseMinMs),
|
|
58
|
+
BURST_PAUSE_MAX: String(cfg.burstPauseMaxMs),
|
|
59
|
+
TYPING_SIMULATION: String(cfg.typingSimulation),
|
|
60
|
+
DAILY_SEND_LIMIT: String(cfg.dailySendLimit),
|
|
61
|
+
CHECK_RECIPIENT: String(cfg.checkRecipientExists),
|
|
243
62
|
};
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
queue = new p_queue_1.default({ concurrency: 1 });
|
|
63
|
+
// Use nohup + background for Docker compatibility
|
|
64
|
+
const cmd = `nohup node "${serverPath}" > /tmp/whatsapp-server-${port}.log 2>&1 &`;
|
|
65
|
+
try {
|
|
66
|
+
(0, node_child_process_1.execSync)(cmd, { env, stdio: 'ignore' });
|
|
249
67
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const hasSession = node_fs_1.default.existsSync(resolvedPath) && node_fs_1.default.readdirSync(resolvedPath).length > 0;
|
|
253
|
-
if (!hasSession) {
|
|
254
|
-
throw new n8n_workflow_1.NodeApiError({}, {
|
|
255
|
-
message: 'No WhatsApp session found. Run the WhatsApp Connect node first to scan QR code.',
|
|
256
|
-
});
|
|
68
|
+
catch (err) {
|
|
69
|
+
throw new Error(`Failed to start WhatsApp server: ${err.message}`);
|
|
257
70
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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') {
|
|
71
|
+
serverPort = port;
|
|
72
|
+
// Wait for server to start (up to 10s)
|
|
73
|
+
const started = await new Promise((resolve) => {
|
|
74
|
+
const start = Date.now();
|
|
75
|
+
const check = setInterval(async () => {
|
|
76
|
+
if (await isPortOpen(port)) {
|
|
278
77
|
clearInterval(check);
|
|
279
78
|
resolve(true);
|
|
280
79
|
}
|
|
281
|
-
if (
|
|
80
|
+
if (Date.now() - start > 10000) {
|
|
282
81
|
clearInterval(check);
|
|
283
82
|
resolve(false);
|
|
284
83
|
}
|
|
285
84
|
}, 500);
|
|
286
|
-
setTimeout(() => { clearInterval(check); resolve(false); }, 20000);
|
|
287
|
-
});
|
|
288
|
-
if (!connected) {
|
|
289
|
-
throw new n8n_workflow_1.NodeApiError({}, {
|
|
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;
|
|
297
|
-
}
|
|
298
|
-
return sock;
|
|
299
|
-
}
|
|
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
85
|
});
|
|
356
|
-
if (!
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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');
|
|
86
|
+
if (!started) {
|
|
87
|
+
// Read log for error details
|
|
88
|
+
let logContent = '';
|
|
89
|
+
try {
|
|
90
|
+
logContent = node_fs_1.default.readFileSync(`/tmp/whatsapp-server-${port}.log`, 'utf-8');
|
|
452
91
|
}
|
|
453
|
-
|
|
454
|
-
|
|
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 };
|
|
92
|
+
catch { }
|
|
93
|
+
throw new Error(`WhatsApp server failed to start. Log: ${logContent.slice(0, 500)}`);
|
|
470
94
|
}
|
|
471
|
-
const w = winner;
|
|
472
|
-
if (w.error) {
|
|
473
|
-
throw w.error;
|
|
474
|
-
}
|
|
475
|
-
return w.result;
|
|
476
95
|
}
|
|
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
96
|
async function disconnect() {
|
|
491
|
-
|
|
97
|
+
// Signal server to stop (it's a separate process)
|
|
98
|
+
if (serverPort) {
|
|
492
99
|
try {
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
catch {
|
|
496
|
-
// ignore
|
|
100
|
+
await fetch(`http://127.0.0.1:${serverPort}/health`);
|
|
497
101
|
}
|
|
102
|
+
catch { }
|
|
498
103
|
}
|
|
499
|
-
socketInstance = null;
|
|
500
|
-
socketStatus = 'stopped';
|
|
501
|
-
++generation; // Invalidate any pending handlers
|
|
502
104
|
}
|
|
503
105
|
function parseIncomingMessage(msg) {
|
|
504
106
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
@@ -542,7 +144,6 @@ function parseIncomingMessage(msg) {
|
|
|
542
144
|
return {
|
|
543
145
|
messageId: msg.key.id,
|
|
544
146
|
chatJid: msg.key.remoteJid,
|
|
545
|
-
// C4: Simplified sender logic — participant is sender in groups, remoteJid in DMs
|
|
546
147
|
sender: (_g = msg.key.participant) !== null && _g !== void 0 ? _g : msg.key.remoteJid,
|
|
547
148
|
content: content || `[${messageType}]`,
|
|
548
149
|
timestamp: new Date(timestampSeconds * 1000).toISOString(),
|
|
@@ -550,3 +151,17 @@ function parseIncomingMessage(msg) {
|
|
|
550
151
|
messageType,
|
|
551
152
|
};
|
|
552
153
|
}
|
|
154
|
+
async function getWhatsAppCredentials(credentials) {
|
|
155
|
+
var _a, _b;
|
|
156
|
+
return {
|
|
157
|
+
sessionPath: credentials.sessionPath || '~/.n8n/whatsapp-auth',
|
|
158
|
+
messageDelayMinMs: credentials.messageDelayMinMs || 5000,
|
|
159
|
+
messageDelayMaxMs: credentials.messageDelayMaxMs || 9000,
|
|
160
|
+
burstSize: (_a = credentials.burstSize) !== null && _a !== void 0 ? _a : 20,
|
|
161
|
+
burstPauseMinMs: credentials.burstPauseMinMs || 30000,
|
|
162
|
+
burstPauseMaxMs: credentials.burstPauseMaxMs || 60000,
|
|
163
|
+
typingSimulation: credentials.typingSimulation !== false,
|
|
164
|
+
dailySendLimit: (_b = credentials.dailySendLimit) !== null && _b !== void 0 ? _b : 500,
|
|
165
|
+
checkRecipientExists: credentials.checkRecipientExists !== false,
|
|
166
|
+
};
|
|
167
|
+
}
|