@raevon/n8n-nodes-whatsapp 2.0.15 → 3.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 +17 -5
- package/dist/nodes/WhatsApp/WhatsAppApiHelper.js +262 -351
- package/dist/nodes/WhatsApp/WhatsAppConnect.node.js +9 -89
- package/dist/nodes/WhatsApp/WhatsAppSend.node.js +29 -114
- package/dist/nodes/WhatsApp/WhatsAppTrigger.node.d.ts +2 -2
- package/dist/nodes/WhatsApp/WhatsAppTrigger.node.js +60 -27
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type WASocket } from '@whiskeysockets/baileys';
|
|
1
2
|
export interface WhatsAppCredentials {
|
|
2
3
|
sessionPath: string;
|
|
3
4
|
messageDelayMinMs: number;
|
|
@@ -9,9 +10,20 @@ export interface WhatsAppCredentials {
|
|
|
9
10
|
dailySendLimit: number;
|
|
10
11
|
checkRecipientExists: boolean;
|
|
11
12
|
}
|
|
12
|
-
export declare function getServerUrl(cfg: WhatsAppCredentials): string;
|
|
13
|
-
export declare function ensureServerRunning(cfg: WhatsAppCredentials): Promise<void>;
|
|
14
|
-
export declare function startServerIfNeeded(cfg: WhatsAppCredentials): Promise<void>;
|
|
15
|
-
export declare function stopServer(cfg: WhatsAppCredentials): Promise<void>;
|
|
16
|
-
export declare function disconnect(): Promise<void>;
|
|
17
13
|
export declare function getWhatsAppCredentials(credentials: Record<string, any>): Promise<WhatsAppCredentials>;
|
|
14
|
+
export declare function ensureConnected(cfg: WhatsAppCredentials): Promise<WASocket>;
|
|
15
|
+
export declare function getConnectionStatus(): {
|
|
16
|
+
status: "stopped" | "connecting" | "qr_ready" | "connected" | "logged_out" | "error" | "disconnected";
|
|
17
|
+
connected: boolean;
|
|
18
|
+
qrAvailable: boolean;
|
|
19
|
+
sentToday: number;
|
|
20
|
+
dailyLimit: number;
|
|
21
|
+
lastError: string | null;
|
|
22
|
+
};
|
|
23
|
+
export declare function connectOrGetQr(cfg: WhatsAppCredentials): Promise<{
|
|
24
|
+
connected: boolean;
|
|
25
|
+
qrUrl?: string;
|
|
26
|
+
message: string;
|
|
27
|
+
}>;
|
|
28
|
+
export declare function parseIncomingMessage(msg: any): Record<string, any> | null;
|
|
29
|
+
export declare function sendMessageWithAntiBan(to: string, content: any, cfg: WhatsAppCredentials): Promise<any>;
|
|
@@ -36,380 +36,130 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.getServerUrl = getServerUrl;
|
|
40
|
-
exports.ensureServerRunning = ensureServerRunning;
|
|
41
|
-
exports.startServerIfNeeded = startServerIfNeeded;
|
|
42
|
-
exports.stopServer = stopServer;
|
|
43
|
-
exports.disconnect = disconnect;
|
|
44
39
|
exports.getWhatsAppCredentials = getWhatsAppCredentials;
|
|
40
|
+
exports.ensureConnected = ensureConnected;
|
|
41
|
+
exports.getConnectionStatus = getConnectionStatus;
|
|
42
|
+
exports.connectOrGetQr = connectOrGetQr;
|
|
43
|
+
exports.parseIncomingMessage = parseIncomingMessage;
|
|
44
|
+
exports.sendMessageWithAntiBan = sendMessageWithAntiBan;
|
|
45
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
46
|
+
const baileys_1 = __importStar(require("@whiskeysockets/baileys"));
|
|
45
47
|
const node_path_1 = __importDefault(require("node:path"));
|
|
46
48
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
// Dynamic imports for Baileys (loaded only when server starts)
|
|
51
|
-
let baileysModule = null;
|
|
49
|
+
const qrcode_1 = __importDefault(require("qrcode"));
|
|
50
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
51
|
+
const randBetween = (min, max) => max > min ? min + Math.floor(Math.random() * (max - min + 1)) : min;
|
|
52
52
|
function expandHome(p) {
|
|
53
53
|
if (!p.startsWith('~'))
|
|
54
54
|
return p;
|
|
55
55
|
return node_path_1.default.join(process.env.HOME || process.env.USERPROFILE || '', p.slice(1));
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
const noop = () => { };
|
|
58
|
+
const silentLogger = { level: 'silent', info: noop, warn: noop, error: noop, debug: noop, fatal: noop, trace: noop, child: () => silentLogger };
|
|
59
|
+
// --- Singleton state ---
|
|
60
|
+
let socket = null;
|
|
61
|
+
let status = 'stopped';
|
|
62
|
+
let queue = null;
|
|
63
|
+
let nextSendAt = 0;
|
|
64
|
+
let sentInBurst = 0;
|
|
65
|
+
let sentTodayCount = 0;
|
|
66
|
+
let sentTodayDate = '';
|
|
67
|
+
let reconnectAttempts = 0;
|
|
68
|
+
let reconnectTimer = null;
|
|
69
|
+
let qrResolve = null;
|
|
70
|
+
let generation = 0;
|
|
71
|
+
let lastDisconnectError = null;
|
|
72
|
+
function todayStartIso() {
|
|
73
|
+
return new Date().toISOString().slice(0, 10) + 'T00:00:00.000Z';
|
|
60
74
|
}
|
|
61
|
-
function
|
|
62
|
-
return
|
|
75
|
+
function sendGapMs(cfg) {
|
|
76
|
+
return randBetween(cfg.messageDelayMinMs, cfg.messageDelayMaxMs);
|
|
63
77
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
function burstPauseMs(cfg) {
|
|
79
|
+
if (!cfg.burstSize || ++sentInBurst < cfg.burstSize)
|
|
80
|
+
return 0;
|
|
81
|
+
sentInBurst = 0;
|
|
82
|
+
return randBetween(cfg.burstPauseMinMs, cfg.burstPauseMaxMs);
|
|
83
|
+
}
|
|
84
|
+
function normalizeRecipient(to) {
|
|
85
|
+
if (to.endsWith('@s.whatsapp.net') || to.endsWith('@g.us'))
|
|
86
|
+
return to;
|
|
87
|
+
const digits = to.replace(/\D/g, '');
|
|
88
|
+
if (!/^[1-9][0-9]{7,14}$/.test(digits))
|
|
89
|
+
throw new Error(`Invalid phone number: ${to}`);
|
|
90
|
+
return `${digits}@s.whatsapp.net`;
|
|
71
91
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
async function startServer(cfg) {
|
|
75
|
-
const port = getPort(cfg);
|
|
76
|
-
// Already running?
|
|
77
|
-
if (servers.has(port))
|
|
92
|
+
async function scheduleReconnect(cfg) {
|
|
93
|
+
if (reconnectTimer)
|
|
78
94
|
return;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
95
|
+
const base = Math.min(1000 * (2 ** reconnectAttempts), 60000);
|
|
96
|
+
const delay = Math.round(base * (0.8 + Math.random() * 0.4));
|
|
97
|
+
reconnectAttempts += 1;
|
|
98
|
+
reconnectTimer = setTimeout(() => { reconnectTimer = null; initSocket(cfg, sessionPathGlobal).catch(() => { }); }, delay);
|
|
99
|
+
}
|
|
100
|
+
let sessionPathGlobal = null;
|
|
101
|
+
async function initSocket(cfg, authPath) {
|
|
102
|
+
if (socket && status === 'connected')
|
|
103
|
+
return socket;
|
|
104
|
+
const gen = ++generation;
|
|
105
|
+
const resolvedPath = expandHome(authPath);
|
|
106
|
+
if (!node_fs_1.default.existsSync(resolvedPath))
|
|
107
|
+
node_fs_1.default.mkdirSync(resolvedPath, { recursive: true });
|
|
108
|
+
const { state, saveCreds } = await (0, baileys_1.useMultiFileAuthState)(resolvedPath);
|
|
109
|
+
const { version } = await (0, baileys_1.fetchLatestBaileysVersion)();
|
|
110
|
+
const sock = (0, baileys_1.default)({
|
|
111
|
+
version,
|
|
112
|
+
browser: baileys_1.Browsers.ubuntu('n8n WhatsApp Node'),
|
|
113
|
+
auth: { creds: state.creds, keys: (0, baileys_1.makeCacheableSignalKeyStore)(state.keys, silentLogger) },
|
|
114
|
+
markOnlineOnConnect: false,
|
|
115
|
+
syncFullHistory: false,
|
|
116
|
+
shouldSyncHistoryMessage: () => false,
|
|
117
|
+
generateHighQualityLinkPreview: false,
|
|
118
|
+
logger: silentLogger,
|
|
119
|
+
});
|
|
120
|
+
socket = sock;
|
|
121
|
+
status = 'connecting';
|
|
122
|
+
reconnectAttempts = 0;
|
|
123
|
+
sock.ev.on('creds.update', () => {
|
|
124
|
+
if (gen !== generation)
|
|
101
125
|
return;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
browser: Browsers.ubuntu('n8n WhatsApp Node'),
|
|
113
|
-
auth: { creds: authState.creds, keys: makeCacheableSignalKeyStore(authState.keys, silentLogger) },
|
|
114
|
-
markOnlineOnConnect: false,
|
|
115
|
-
syncFullHistory: false,
|
|
116
|
-
shouldSyncHistoryMessage: () => false,
|
|
117
|
-
generateHighQualityLinkPreview: false,
|
|
118
|
-
logger: silentLogger,
|
|
119
|
-
});
|
|
120
|
-
state.socket.ev.on('creds.update', () => saveCreds().catch(() => { }));
|
|
121
|
-
state.socket.ev.on('connection.update', (update) => {
|
|
122
|
-
var _a, _b;
|
|
123
|
-
const { connection, lastDisconnect, qr: newQr } = update;
|
|
124
|
-
if (newQr) {
|
|
125
|
-
state.qr = newQr;
|
|
126
|
-
state.status = 'qr_ready';
|
|
127
|
-
}
|
|
128
|
-
if (connection === 'open') {
|
|
129
|
-
state.status = 'connected';
|
|
130
|
-
state.qr = null;
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (connection === 'close') {
|
|
134
|
-
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;
|
|
135
|
-
if (code === DisconnectReason.loggedOut) {
|
|
136
|
-
state.status = 'logged_out';
|
|
137
|
-
state.socket = null;
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
state.status = 'disconnected';
|
|
141
|
-
state.socket = null;
|
|
142
|
-
setTimeout(() => connect().catch(() => { }), 5000);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
state.socket.ev.on('messages.upsert', (upsert) => {
|
|
147
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
148
|
-
const { messages, type } = upsert;
|
|
149
|
-
if (type !== 'notify')
|
|
150
|
-
return;
|
|
151
|
-
for (const msg of messages) {
|
|
152
|
-
if (msg.key.fromMe)
|
|
153
|
-
continue;
|
|
154
|
-
let content = null;
|
|
155
|
-
const msgType = Object.keys(msg.message || {})[0] || 'unknown';
|
|
156
|
-
if ((_a = msg.message) === null || _a === void 0 ? void 0 : _a.conversation)
|
|
157
|
-
content = msg.message.conversation;
|
|
158
|
-
else if ((_c = (_b = msg.message) === null || _b === void 0 ? void 0 : _b.extendedTextMessage) === null || _c === void 0 ? void 0 : _c.text)
|
|
159
|
-
content = msg.message.extendedTextMessage.text;
|
|
160
|
-
else if ((_e = (_d = msg.message) === null || _d === void 0 ? void 0 : _d.imageMessage) === null || _e === void 0 ? void 0 : _e.caption)
|
|
161
|
-
content = msg.message.imageMessage.caption;
|
|
162
|
-
else if ((_g = (_f = msg.message) === null || _f === void 0 ? void 0 : _f.videoMessage) === null || _g === void 0 ? void 0 : _g.caption)
|
|
163
|
-
content = msg.message.videoMessage.caption;
|
|
164
|
-
else if ((_j = (_h = msg.message) === null || _h === void 0 ? void 0 : _h.documentMessage) === null || _j === void 0 ? void 0 : _j.fileName)
|
|
165
|
-
content = msg.message.documentMessage.fileName;
|
|
166
|
-
else if ((_k = msg.message) === null || _k === void 0 ? void 0 : _k.audioMessage)
|
|
167
|
-
content = '[Audio]';
|
|
168
|
-
else
|
|
169
|
-
content = `[${msgType}]`;
|
|
170
|
-
const ts = msg.messageTimestamp != null ? Number(msg.messageTimestamp) : Date.now() / 1000;
|
|
171
|
-
state.messageBuffer.push({ messageId: msg.key.id, chatJid: msg.key.remoteJid, sender: (_l = msg.key.participant) !== null && _l !== void 0 ? _l : msg.key.remoteJid, content, timestamp: new Date(ts * 1000).toISOString(), isFromMe: false, messageType: msgType });
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
if (!state.queue) {
|
|
175
|
-
const PQueue = (await Promise.resolve().then(() => __importStar(require('p-queue')))).default;
|
|
176
|
-
state.queue = new PQueue({ concurrency: 1 });
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
function normalizeRecipient(to) {
|
|
180
|
-
if (to.endsWith('@s.whatsapp.net') || to.endsWith('@g.us'))
|
|
181
|
-
return to;
|
|
182
|
-
const digits = to.replace(/\D/g, '');
|
|
183
|
-
if (!/^[1-9][0-9]{7,14}$/.test(digits))
|
|
184
|
-
throw new Error(`Invalid phone number: ${to}`);
|
|
185
|
-
return `${digits}@s.whatsapp.net`;
|
|
186
|
-
}
|
|
187
|
-
async function sendMessage(to, content) {
|
|
188
|
-
var _a;
|
|
189
|
-
if (!state.socket || state.status !== 'connected')
|
|
190
|
-
throw new Error('WhatsApp not connected');
|
|
191
|
-
if (cfg.dailySendLimit > 0) {
|
|
192
|
-
const today = new Date().toISOString().slice(0, 10) + 'T00:00:00.000Z';
|
|
193
|
-
if (state.sentTodayDate !== today) {
|
|
194
|
-
state.sentTodayDate = today;
|
|
195
|
-
state.sentTodayCount = 0;
|
|
196
|
-
}
|
|
197
|
-
if (state.sentTodayCount >= cfg.dailySendLimit)
|
|
198
|
-
throw new Error(`Daily limit (${cfg.dailySendLimit}) reached`);
|
|
199
|
-
}
|
|
200
|
-
const jid = normalizeRecipient(to);
|
|
201
|
-
if (cfg.checkRecipientExists && !jid.endsWith('@g.us')) {
|
|
202
|
-
const results = await state.socket.onWhatsApp(jid);
|
|
203
|
-
if (!((_a = results === null || results === void 0 ? void 0 : results[0]) === null || _a === void 0 ? void 0 : _a.exists))
|
|
204
|
-
throw new Error(`Recipient ${to} not on WhatsApp`);
|
|
205
|
-
}
|
|
206
|
-
const task = state.queue.add(async () => {
|
|
207
|
-
var _a;
|
|
208
|
-
const wait = state.nextSendAt - Date.now();
|
|
209
|
-
if (wait > 0)
|
|
210
|
-
await sleep(wait);
|
|
211
|
-
if (cfg.typingSimulation) {
|
|
212
|
-
try {
|
|
213
|
-
await state.socket.sendPresenceUpdate('composing', jid);
|
|
214
|
-
const len = (content.text || content.caption || '').length;
|
|
215
|
-
await sleep(Math.min(randBetween(900, 1800) + len * 25, 4000));
|
|
216
|
-
await state.socket.sendPresenceUpdate('paused', jid);
|
|
217
|
-
}
|
|
218
|
-
catch { }
|
|
219
|
-
}
|
|
220
|
-
const response = await state.socket.sendMessage(jid, content);
|
|
221
|
-
const gap = randBetween(cfg.messageDelayMinMs, cfg.messageDelayMaxMs);
|
|
222
|
-
let burst = 0;
|
|
223
|
-
if (cfg.burstSize && ++state.sentInBurst >= cfg.burstSize) {
|
|
224
|
-
state.sentInBurst = 0;
|
|
225
|
-
burst = randBetween(cfg.burstPauseMinMs, cfg.burstPauseMaxMs);
|
|
226
|
-
}
|
|
227
|
-
state.nextSendAt = Date.now() + gap + burst;
|
|
228
|
-
if (cfg.dailySendLimit > 0)
|
|
229
|
-
state.sentTodayCount++;
|
|
230
|
-
if (!((_a = response === null || response === void 0 ? void 0 : response.key) === null || _a === void 0 ? void 0 : _a.id))
|
|
231
|
-
throw new Error('No message ID returned');
|
|
232
|
-
return { messageId: response.key.id, status: 'sent', recipient: jid };
|
|
233
|
-
});
|
|
234
|
-
let timer;
|
|
235
|
-
const winner = await Promise.race([
|
|
236
|
-
task.then((r) => ({ result: r }), (e) => ({ error: e })),
|
|
237
|
-
new Promise(r => { timer = setTimeout(() => r(null), 15000); }),
|
|
238
|
-
]);
|
|
239
|
-
clearTimeout(timer);
|
|
240
|
-
if (!winner) {
|
|
241
|
-
task.then(() => { }, () => { });
|
|
242
|
-
return { messageId: 'queued', status: 'queued', recipient: jid };
|
|
126
|
+
saveCreds().catch(() => { });
|
|
127
|
+
});
|
|
128
|
+
sock.ev.on('connection.update', async (update) => {
|
|
129
|
+
var _a, _b;
|
|
130
|
+
if (gen !== generation)
|
|
131
|
+
return;
|
|
132
|
+
const { connection, lastDisconnect, qr: newQr } = update;
|
|
133
|
+
if (newQr) {
|
|
134
|
+
qrResolve && qrResolve({ qr: newQr, qrUrl: '' });
|
|
135
|
+
qrResolve = null;
|
|
243
136
|
}
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
// HTTP server
|
|
249
|
-
const httpServer = node_http_1.default.createServer(async (req, res) => {
|
|
250
|
-
const url = new node_url_1.URL(req.url || '/', `http://localhost:${port}`);
|
|
251
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
252
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
253
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
254
|
-
if (req.method === 'OPTIONS') {
|
|
255
|
-
res.writeHead(204);
|
|
256
|
-
res.end();
|
|
137
|
+
if (connection === 'open') {
|
|
138
|
+
status = 'connected';
|
|
139
|
+
reconnectAttempts = 0;
|
|
257
140
|
return;
|
|
258
141
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
res.end(JSON.stringify({ status: state.status, connected: state.status === 'connected', qrAvailable: state.status === 'qr_ready', sentToday: state.sentTodayCount, dailyLimit: cfg.dailySendLimit }));
|
|
267
|
-
}
|
|
268
|
-
else if (url.pathname === '/connect') {
|
|
269
|
-
await connect();
|
|
270
|
-
const result = await new Promise(r => {
|
|
271
|
-
const check = setInterval(() => {
|
|
272
|
-
if (state.status === 'connected') {
|
|
273
|
-
clearInterval(check);
|
|
274
|
-
r({ connected: true, message: 'Connected' });
|
|
275
|
-
}
|
|
276
|
-
if (state.qr) {
|
|
277
|
-
clearInterval(check);
|
|
278
|
-
Promise.resolve().then(() => __importStar(require('qrcode'))).then(QRCode => QRCode.toDataURL(state.qr, { width: 300, margin: 2 }).then(qrUrl => r({ connected: false, qrUrl, message: 'Scan QR' })));
|
|
279
|
-
}
|
|
280
|
-
}, 500);
|
|
281
|
-
setTimeout(() => { clearInterval(check); r({ connected: false, status: state.status, message: 'Timeout' }); }, 30000);
|
|
282
|
-
});
|
|
283
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
284
|
-
res.end(JSON.stringify(result));
|
|
285
|
-
}
|
|
286
|
-
else if (url.pathname === '/qr' && state.qr) {
|
|
287
|
-
const QRCode = await Promise.resolve().then(() => __importStar(require('qrcode')));
|
|
288
|
-
const qrUrl = await QRCode.toDataURL(state.qr, { width: 400, margin: 2 });
|
|
289
|
-
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
290
|
-
res.end(`<!DOCTYPE html><html><body style="display:flex;justify-content:center;align-items:center;height:100vh;background:#111"><img src="${qrUrl}"/></body></html>`);
|
|
291
|
-
}
|
|
292
|
-
else if (url.pathname === '/send' && req.method === 'POST') {
|
|
293
|
-
let body = '';
|
|
294
|
-
req.on('data', chunk => body += chunk);
|
|
295
|
-
req.on('end', async () => {
|
|
296
|
-
try {
|
|
297
|
-
const { to, ...content } = JSON.parse(body);
|
|
298
|
-
if (!to)
|
|
299
|
-
throw new Error('Missing "to"');
|
|
300
|
-
const result = await sendMessage(to, content);
|
|
301
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
302
|
-
res.end(JSON.stringify(result));
|
|
303
|
-
}
|
|
304
|
-
catch (err) {
|
|
305
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
306
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
else if (url.pathname === '/messages') {
|
|
311
|
-
const msgs = state.messageBuffer.splice(0, state.messageBuffer.length);
|
|
312
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
313
|
-
res.end(JSON.stringify({ messages: msgs, count: msgs.length }));
|
|
314
|
-
}
|
|
315
|
-
else if (url.pathname === '/signout') {
|
|
316
|
-
if (state.socket) {
|
|
317
|
-
try {
|
|
318
|
-
state.socket.end(new Error('Sign out'));
|
|
319
|
-
}
|
|
320
|
-
catch { }
|
|
321
|
-
state.socket = null;
|
|
322
|
-
}
|
|
323
|
-
state.status = 'stopped';
|
|
324
|
-
const p = expandHome(cfg.sessionPath);
|
|
325
|
-
if (node_fs_1.default.existsSync(p)) {
|
|
326
|
-
node_fs_1.default.readdirSync(p).forEach(f => node_fs_1.default.unlinkSync(node_path_1.default.join(p, f)));
|
|
327
|
-
node_fs_1.default.rmdirSync(p);
|
|
328
|
-
}
|
|
329
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
330
|
-
res.end(JSON.stringify({ success: true, message: 'Signed out' }));
|
|
331
|
-
}
|
|
332
|
-
else if (url.pathname === '/shutdown') {
|
|
333
|
-
if (state.socket) {
|
|
334
|
-
try {
|
|
335
|
-
state.socket.end(new Error('Shutdown'));
|
|
336
|
-
}
|
|
337
|
-
catch { }
|
|
338
|
-
}
|
|
339
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
340
|
-
res.end(JSON.stringify({ success: true }));
|
|
341
|
-
// Close HTTP server after sending response
|
|
342
|
-
setTimeout(() => { httpServer.close(); servers.delete(port); }, 500);
|
|
142
|
+
if (connection === 'close') {
|
|
143
|
+
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;
|
|
144
|
+
lastDisconnectError = `Code: ${code}`;
|
|
145
|
+
if (code === baileys_1.DisconnectReason.loggedOut) {
|
|
146
|
+
status = 'logged_out';
|
|
147
|
+
socket = null;
|
|
148
|
+
++generation;
|
|
343
149
|
}
|
|
344
150
|
else {
|
|
345
|
-
|
|
346
|
-
|
|
151
|
+
status = 'disconnected';
|
|
152
|
+
socket = null;
|
|
153
|
+
scheduleReconnect(cfg);
|
|
347
154
|
}
|
|
348
155
|
}
|
|
349
|
-
catch (err) {
|
|
350
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
351
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
352
|
-
}
|
|
353
156
|
});
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
console.log(`[WhatsApp] Server started on port ${port}`);
|
|
358
|
-
}
|
|
359
|
-
async function ensureServerRunning(cfg) {
|
|
360
|
-
const port = getPort(cfg);
|
|
361
|
-
// Just check if server is running — don't start it
|
|
362
|
-
try {
|
|
363
|
-
const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(3000) });
|
|
364
|
-
if (res.ok)
|
|
365
|
-
return; // Server alive
|
|
157
|
+
if (!queue) {
|
|
158
|
+
const PQ = (await Promise.resolve().then(() => __importStar(require('p-queue')))).default;
|
|
159
|
+
queue = new PQ({ concurrency: 1 });
|
|
366
160
|
}
|
|
367
|
-
|
|
368
|
-
throw new Error(`WhatsApp server not running on port ${port}. Run "Start Server" or "Connect" first.`);
|
|
161
|
+
return sock;
|
|
369
162
|
}
|
|
370
|
-
async function startServerIfNeeded(cfg) {
|
|
371
|
-
const port = getPort(cfg);
|
|
372
|
-
// Check if already running
|
|
373
|
-
try {
|
|
374
|
-
const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(3000) });
|
|
375
|
-
if (res.ok)
|
|
376
|
-
return; // Already running
|
|
377
|
-
}
|
|
378
|
-
catch { }
|
|
379
|
-
// Port free — start server
|
|
380
|
-
if (await isPortOpen(port)) {
|
|
381
|
-
await startServer(cfg);
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
// Port occupied by something else
|
|
385
|
-
throw new Error(`Port ${port} occupied. Restart n8n container, then try again.`);
|
|
386
|
-
}
|
|
387
|
-
async function stopServer(cfg) {
|
|
388
|
-
const port = getPort(cfg);
|
|
389
|
-
// Try to shut down the server via /shutdown endpoint
|
|
390
|
-
try {
|
|
391
|
-
await fetch(`http://127.0.0.1:${port}/shutdown`, { method: 'POST', signal: AbortSignal.timeout(3000) });
|
|
392
|
-
}
|
|
393
|
-
catch { }
|
|
394
|
-
// Clean up in-memory state
|
|
395
|
-
const state = servers.get(port);
|
|
396
|
-
if (state) {
|
|
397
|
-
if (state.socket) {
|
|
398
|
-
try {
|
|
399
|
-
state.socket.end(new Error('Server stop'));
|
|
400
|
-
}
|
|
401
|
-
catch { }
|
|
402
|
-
}
|
|
403
|
-
if (state.server) {
|
|
404
|
-
try {
|
|
405
|
-
state.server.close();
|
|
406
|
-
}
|
|
407
|
-
catch { }
|
|
408
|
-
}
|
|
409
|
-
servers.delete(port);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
async function disconnect() { }
|
|
413
163
|
async function getWhatsAppCredentials(credentials) {
|
|
414
164
|
var _a, _b;
|
|
415
165
|
return {
|
|
@@ -424,3 +174,164 @@ async function getWhatsAppCredentials(credentials) {
|
|
|
424
174
|
checkRecipientExists: credentials.checkRecipientExists !== false,
|
|
425
175
|
};
|
|
426
176
|
}
|
|
177
|
+
async function ensureConnected(cfg) {
|
|
178
|
+
sessionPathGlobal = cfg.sessionPath;
|
|
179
|
+
const resolvedPath = expandHome(cfg.sessionPath);
|
|
180
|
+
const hasSession = node_fs_1.default.existsSync(resolvedPath) && node_fs_1.default.readdirSync(resolvedPath).length > 0;
|
|
181
|
+
if (!hasSession) {
|
|
182
|
+
// No session — connect and wait for QR
|
|
183
|
+
const sock = await initSocket(cfg, cfg.sessionPath);
|
|
184
|
+
const qrData = await new Promise((resolve) => {
|
|
185
|
+
qrResolve = resolve;
|
|
186
|
+
setTimeout(() => { if (qrResolve) {
|
|
187
|
+
qrResolve(null);
|
|
188
|
+
qrResolve = null;
|
|
189
|
+
} }, 90000);
|
|
190
|
+
});
|
|
191
|
+
if (!qrData)
|
|
192
|
+
throw new n8n_workflow_1.NodeApiError({}, { message: 'QR expired. Run Connect again.' });
|
|
193
|
+
// Wait for connection
|
|
194
|
+
await new Promise((resolve, reject) => {
|
|
195
|
+
const timeout = setTimeout(() => reject(new Error('Connection timeout')), 30000);
|
|
196
|
+
const check = setInterval(() => {
|
|
197
|
+
if (status === 'connected') {
|
|
198
|
+
clearTimeout(timeout);
|
|
199
|
+
clearInterval(check);
|
|
200
|
+
resolve();
|
|
201
|
+
}
|
|
202
|
+
if (status === 'logged_out' || status === 'error') {
|
|
203
|
+
clearTimeout(timeout);
|
|
204
|
+
clearInterval(check);
|
|
205
|
+
reject(new Error('Connection failed'));
|
|
206
|
+
}
|
|
207
|
+
}, 1000);
|
|
208
|
+
});
|
|
209
|
+
return sock;
|
|
210
|
+
}
|
|
211
|
+
// Session exists — connect
|
|
212
|
+
const sock = await initSocket(cfg, cfg.sessionPath);
|
|
213
|
+
await new Promise((resolve) => {
|
|
214
|
+
const check = setInterval(() => {
|
|
215
|
+
if (status === 'connected' || status === 'error' || status === 'logged_out') {
|
|
216
|
+
clearInterval(check);
|
|
217
|
+
resolve();
|
|
218
|
+
}
|
|
219
|
+
}, 500);
|
|
220
|
+
setTimeout(() => { clearInterval(check); resolve(); }, 20000);
|
|
221
|
+
});
|
|
222
|
+
if (status !== 'connected')
|
|
223
|
+
throw new n8n_workflow_1.NodeApiError({}, { message: `Connection failed (status: ${status})` });
|
|
224
|
+
return sock;
|
|
225
|
+
}
|
|
226
|
+
function getConnectionStatus() {
|
|
227
|
+
return { status, connected: status === 'connected', qrAvailable: status === 'qr_ready', sentToday: sentTodayCount, dailyLimit: 500, lastError: lastDisconnectError };
|
|
228
|
+
}
|
|
229
|
+
async function connectOrGetQr(cfg) {
|
|
230
|
+
sessionPathGlobal = cfg.sessionPath;
|
|
231
|
+
const resolvedPath = expandHome(cfg.sessionPath);
|
|
232
|
+
const hasSession = node_fs_1.default.existsSync(resolvedPath) && node_fs_1.default.readdirSync(resolvedPath).length > 0;
|
|
233
|
+
if (hasSession) {
|
|
234
|
+
await initSocket(cfg, cfg.sessionPath);
|
|
235
|
+
await new Promise((resolve) => {
|
|
236
|
+
const check = setInterval(() => { if (status === 'connected' || status === 'error' || status === 'logged_out') {
|
|
237
|
+
clearInterval(check);
|
|
238
|
+
resolve();
|
|
239
|
+
} }, 500);
|
|
240
|
+
setTimeout(() => { clearInterval(check); resolve(); }, 15000);
|
|
241
|
+
});
|
|
242
|
+
if (status === 'connected')
|
|
243
|
+
return { connected: true, message: 'Connected successfully' };
|
|
244
|
+
return { connected: false, message: `Status: ${status}` };
|
|
245
|
+
}
|
|
246
|
+
const sock = await initSocket(cfg, cfg.sessionPath);
|
|
247
|
+
const qrData = await new Promise((resolve) => {
|
|
248
|
+
qrResolve = resolve;
|
|
249
|
+
setTimeout(() => { if (qrResolve) {
|
|
250
|
+
qrResolve(null);
|
|
251
|
+
qrResolve = null;
|
|
252
|
+
} }, 30000);
|
|
253
|
+
});
|
|
254
|
+
if (!qrData)
|
|
255
|
+
return { connected: false, message: 'QR not generated. Try again.' };
|
|
256
|
+
const qrDataUrl = await qrcode_1.default.toDataURL(qrData.qr, { width: 300, margin: 2 });
|
|
257
|
+
return { connected: false, qrUrl: qrDataUrl, message: `Open this URL to scan QR:\n${qrDataUrl}\n\nAfter scanning, run Connect again.` };
|
|
258
|
+
}
|
|
259
|
+
function parseIncomingMessage(msg) {
|
|
260
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
261
|
+
if (!msg.message || !msg.key || !msg.key.remoteJid)
|
|
262
|
+
return null;
|
|
263
|
+
let content = null;
|
|
264
|
+
const messageType = Object.keys(msg.message)[0];
|
|
265
|
+
if (msg.message.conversation)
|
|
266
|
+
content = msg.message.conversation;
|
|
267
|
+
else if ((_a = msg.message.extendedTextMessage) === null || _a === void 0 ? void 0 : _a.text)
|
|
268
|
+
content = msg.message.extendedTextMessage.text;
|
|
269
|
+
else if ((_b = msg.message.imageMessage) === null || _b === void 0 ? void 0 : _b.caption)
|
|
270
|
+
content = msg.message.imageMessage.caption;
|
|
271
|
+
else if ((_c = msg.message.videoMessage) === null || _c === void 0 ? void 0 : _c.caption)
|
|
272
|
+
content = msg.message.videoMessage.caption;
|
|
273
|
+
else if ((_d = msg.message.documentMessage) === null || _d === void 0 ? void 0 : _d.fileName)
|
|
274
|
+
content = msg.message.documentMessage.fileName;
|
|
275
|
+
else if (msg.message.audioMessage)
|
|
276
|
+
content = '[Audio]';
|
|
277
|
+
else if (msg.message.stickerMessage)
|
|
278
|
+
content = '[Sticker]';
|
|
279
|
+
else if (msg.message.locationMessage)
|
|
280
|
+
content = `[Location] ${msg.message.locationMessage.address || ''}`;
|
|
281
|
+
else if ((_e = msg.message.contactMessage) === null || _e === void 0 ? void 0 : _e.displayName)
|
|
282
|
+
content = `[Contact] ${msg.message.contactMessage.displayName}`;
|
|
283
|
+
else if ((_f = msg.message.pollCreationMessage) === null || _f === void 0 ? void 0 : _f.name)
|
|
284
|
+
content = `[Poll] ${msg.message.pollCreationMessage.name}`;
|
|
285
|
+
const ts = msg.messageTimestamp != null ? Number(msg.messageTimestamp) : Date.now() / 1000;
|
|
286
|
+
return { messageId: msg.key.id, chatJid: msg.key.remoteJid, sender: (_g = msg.key.participant) !== null && _g !== void 0 ? _g : msg.key.remoteJid, content: content || `[${messageType}]`, timestamp: new Date(ts * 1000).toISOString(), isFromMe: (_h = msg.key.fromMe) !== null && _h !== void 0 ? _h : false, messageType };
|
|
287
|
+
}
|
|
288
|
+
async function sendMessageWithAntiBan(to, content, cfg) {
|
|
289
|
+
var _a;
|
|
290
|
+
const sock = await ensureConnected(cfg);
|
|
291
|
+
if (cfg.dailySendLimit > 0) {
|
|
292
|
+
const today = todayStartIso();
|
|
293
|
+
if (sentTodayDate !== today) {
|
|
294
|
+
sentTodayDate = today;
|
|
295
|
+
sentTodayCount = 0;
|
|
296
|
+
}
|
|
297
|
+
if (sentTodayCount >= cfg.dailySendLimit)
|
|
298
|
+
throw new Error(`Daily limit reached`);
|
|
299
|
+
}
|
|
300
|
+
const jid = normalizeRecipient(to);
|
|
301
|
+
if (cfg.checkRecipientExists && !jid.endsWith('@g.us')) {
|
|
302
|
+
const results = await sock.onWhatsApp(jid);
|
|
303
|
+
if (!((_a = results === null || results === void 0 ? void 0 : results[0]) === null || _a === void 0 ? void 0 : _a.exists))
|
|
304
|
+
throw new Error(`Recipient not on WhatsApp`);
|
|
305
|
+
}
|
|
306
|
+
const task = queue.add(async () => {
|
|
307
|
+
var _a;
|
|
308
|
+
const wait = nextSendAt - Date.now();
|
|
309
|
+
if (wait > 0)
|
|
310
|
+
await sleep(wait);
|
|
311
|
+
if (cfg.typingSimulation) {
|
|
312
|
+
try {
|
|
313
|
+
await sock.sendPresenceUpdate('composing', jid);
|
|
314
|
+
await sleep(Math.min(randBetween(900, 1800) + (content.text || '').length * 25, 4000));
|
|
315
|
+
await sock.sendPresenceUpdate('paused', jid);
|
|
316
|
+
}
|
|
317
|
+
catch { }
|
|
318
|
+
}
|
|
319
|
+
const response = await sock.sendMessage(jid, content);
|
|
320
|
+
nextSendAt = Date.now() + sendGapMs(cfg) + burstPauseMs(cfg);
|
|
321
|
+
if (cfg.dailySendLimit > 0)
|
|
322
|
+
sentTodayCount++;
|
|
323
|
+
if (!((_a = response === null || response === void 0 ? void 0 : response.key) === null || _a === void 0 ? void 0 : _a.id))
|
|
324
|
+
throw new Error('No message ID');
|
|
325
|
+
return { messageId: response.key.id, status: 'sent', recipient: jid };
|
|
326
|
+
});
|
|
327
|
+
let timer;
|
|
328
|
+
const winner = await Promise.race([task.then((r) => ({ result: r }), (e) => ({ error: e })), new Promise(r => { timer = setTimeout(() => r(null), 15000); })]);
|
|
329
|
+
clearTimeout(timer);
|
|
330
|
+
if (!winner) {
|
|
331
|
+
task.then(() => { }, () => { });
|
|
332
|
+
return { messageId: 'queued', status: 'queued', recipient: jid };
|
|
333
|
+
}
|
|
334
|
+
if (winner.error)
|
|
335
|
+
throw winner.error;
|
|
336
|
+
return winner.result;
|
|
337
|
+
}
|
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.WhatsAppConnect = void 0;
|
|
7
4
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
8
5
|
const WhatsAppApiHelper_1 = require("./WhatsAppApiHelper");
|
|
9
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
11
|
-
function expandHome(p) {
|
|
12
|
-
if (!p.startsWith('~'))
|
|
13
|
-
return p;
|
|
14
|
-
return node_path_1.default.join(process.env.HOME || process.env.USERPROFILE || '', p.slice(1));
|
|
15
|
-
}
|
|
16
6
|
class WhatsAppConnect {
|
|
17
7
|
constructor() {
|
|
18
8
|
this.description = {
|
|
@@ -22,23 +12,17 @@ class WhatsAppConnect {
|
|
|
22
12
|
group: ['transform'],
|
|
23
13
|
version: 1,
|
|
24
14
|
subtitle: '={{$parameter["operation"]}}',
|
|
25
|
-
description: 'Connect to WhatsApp —
|
|
15
|
+
description: 'Connect to WhatsApp — scan QR on first run',
|
|
26
16
|
defaults: { name: 'WhatsApp Connect' },
|
|
27
17
|
inputs: ['main'],
|
|
28
18
|
outputs: ['main'],
|
|
29
19
|
credentials: [{ name: 'whatsappApi', required: true }],
|
|
30
20
|
properties: [
|
|
31
21
|
{
|
|
32
|
-
displayName: 'Operation',
|
|
33
|
-
name: 'operation',
|
|
34
|
-
type: 'options',
|
|
35
|
-
noDataExpression: true,
|
|
22
|
+
displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true,
|
|
36
23
|
options: [
|
|
37
|
-
{ name: 'Connect', value: 'connect', description: '
|
|
38
|
-
{ name: 'Get Status', value: 'status', description: 'Get
|
|
39
|
-
{ name: 'Start Server', value: 'startServer', description: 'Start the WhatsApp background server', action: 'Start server' },
|
|
40
|
-
{ name: 'Stop Server', value: 'stopServer', description: 'Stop the WhatsApp background server', action: 'Stop server' },
|
|
41
|
-
{ name: 'Sign Out', value: 'signOut', description: 'Stop server and delete session', action: 'Sign out' },
|
|
24
|
+
{ name: 'Connect', value: 'connect', description: 'Connect to WhatsApp', action: 'Connect' },
|
|
25
|
+
{ name: 'Get Status', value: 'status', description: 'Get connection status', action: 'Get status' },
|
|
42
26
|
],
|
|
43
27
|
default: 'connect',
|
|
44
28
|
},
|
|
@@ -53,77 +37,13 @@ class WhatsAppConnect {
|
|
|
53
37
|
const operation = this.getNodeParameter('operation', 0);
|
|
54
38
|
for (let i = 0; i < items.length; i++) {
|
|
55
39
|
try {
|
|
56
|
-
if (operation === '
|
|
57
|
-
await (0, WhatsAppApiHelper_1.
|
|
58
|
-
returnData.push({
|
|
59
|
-
json: { success: true, message: 'Server started', port: (0, WhatsAppApiHelper_1.getServerUrl)(cfg), sessionPath: cfg.sessionPath },
|
|
60
|
-
pairedItem: { item: i },
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
else if (operation === 'stopServer') {
|
|
64
|
-
try {
|
|
65
|
-
await fetch(`${(0, WhatsAppApiHelper_1.getServerUrl)(cfg)}/shutdown`, { method: 'POST', signal: AbortSignal.timeout(3000) });
|
|
66
|
-
returnData.push({
|
|
67
|
-
json: { success: true, message: 'Server stopped', sessionPath: cfg.sessionPath },
|
|
68
|
-
pairedItem: { item: i },
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
returnData.push({
|
|
73
|
-
json: { success: false, message: 'Could not reach server to stop it.', sessionPath: cfg.sessionPath },
|
|
74
|
-
pairedItem: { item: i },
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
else if (operation === 'connect') {
|
|
79
|
-
try {
|
|
80
|
-
await (0, WhatsAppApiHelper_1.startServerIfNeeded)(cfg);
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
returnData.push({
|
|
84
|
-
json: { connected: false, error: err.message, sessionPath: cfg.sessionPath },
|
|
85
|
-
pairedItem: { item: i },
|
|
86
|
-
});
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
const serverUrl = (0, WhatsAppApiHelper_1.getServerUrl)(cfg);
|
|
91
|
-
const response = await fetch(`${serverUrl}/connect`, { method: 'POST', signal: AbortSignal.timeout(60000) });
|
|
92
|
-
const result = await response.json();
|
|
93
|
-
returnData.push({ json: { ...result, sessionPath: cfg.sessionPath }, pairedItem: { item: i } });
|
|
94
|
-
}
|
|
95
|
-
catch (err) {
|
|
96
|
-
returnData.push({
|
|
97
|
-
json: { connected: false, error: `Connect failed: ${err.message}`, sessionPath: cfg.sessionPath },
|
|
98
|
-
pairedItem: { item: i },
|
|
99
|
-
});
|
|
100
|
-
}
|
|
40
|
+
if (operation === 'connect') {
|
|
41
|
+
const result = await (0, WhatsAppApiHelper_1.connectOrGetQr)(cfg);
|
|
42
|
+
returnData.push({ json: { ...result, sessionPath: cfg.sessionPath }, pairedItem: { item: i } });
|
|
101
43
|
}
|
|
102
44
|
else if (operation === 'status') {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const response = await fetch(`${(0, WhatsAppApiHelper_1.getServerUrl)(cfg)}/status`);
|
|
106
|
-
const result = await response.json();
|
|
107
|
-
returnData.push({ json: { ...result, sessionPath: cfg.sessionPath }, pairedItem: { item: i } });
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
returnData.push({
|
|
111
|
-
json: { status: 'server_not_running', connected: false, sessionPath: cfg.sessionPath },
|
|
112
|
-
pairedItem: { item: i },
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
else if (operation === 'signOut') {
|
|
117
|
-
await (0, WhatsAppApiHelper_1.stopServer)(cfg);
|
|
118
|
-
const resolvedPath = expandHome(cfg.sessionPath);
|
|
119
|
-
if (node_fs_1.default.existsSync(resolvedPath)) {
|
|
120
|
-
node_fs_1.default.readdirSync(resolvedPath).forEach(f => node_fs_1.default.unlinkSync(node_path_1.default.join(resolvedPath, f)));
|
|
121
|
-
node_fs_1.default.rmdirSync(resolvedPath);
|
|
122
|
-
}
|
|
123
|
-
returnData.push({
|
|
124
|
-
json: { success: true, message: 'Signed out. Session deleted.', sessionPath: cfg.sessionPath },
|
|
125
|
-
pairedItem: { item: i },
|
|
126
|
-
});
|
|
45
|
+
const status = (0, WhatsAppApiHelper_1.getConnectionStatus)();
|
|
46
|
+
returnData.push({ json: { ...status, sessionPath: cfg.sessionPath }, pairedItem: { item: i } });
|
|
127
47
|
}
|
|
128
48
|
}
|
|
129
49
|
catch (error) {
|
|
@@ -12,95 +12,31 @@ class WhatsAppSend {
|
|
|
12
12
|
group: ['transform'],
|
|
13
13
|
version: 1,
|
|
14
14
|
subtitle: '={{$parameter["operation"]}}',
|
|
15
|
-
description: 'Send WhatsApp messages
|
|
15
|
+
description: 'Send WhatsApp messages with anti-ban protection',
|
|
16
16
|
defaults: { name: 'WhatsApp Send' },
|
|
17
17
|
inputs: ['main'],
|
|
18
18
|
outputs: ['main'],
|
|
19
19
|
credentials: [{ name: 'whatsappApi', required: true }],
|
|
20
20
|
properties: [
|
|
21
21
|
{
|
|
22
|
-
displayName: 'Operation',
|
|
23
|
-
name: 'operation',
|
|
24
|
-
type: 'options',
|
|
25
|
-
noDataExpression: true,
|
|
22
|
+
displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true,
|
|
26
23
|
options: [
|
|
27
|
-
{ name: 'Send Text', value: 'sendText', description: 'Send a text message', action: 'Send text
|
|
28
|
-
{ name: 'Send Image', value: 'sendImage', description: 'Send an image
|
|
29
|
-
{ name: 'Send Document', value: 'sendDocument', description: 'Send a document
|
|
30
|
-
{ name: 'Send Audio', value: 'sendAudio', description: 'Send
|
|
31
|
-
{ name: 'Send Location', value: 'sendLocation', description: 'Send
|
|
24
|
+
{ name: 'Send Text', value: 'sendText', description: 'Send a text message', action: 'Send text' },
|
|
25
|
+
{ name: 'Send Image', value: 'sendImage', description: 'Send an image', action: 'Send image' },
|
|
26
|
+
{ name: 'Send Document', value: 'sendDocument', description: 'Send a document', action: 'Send document' },
|
|
27
|
+
{ name: 'Send Audio', value: 'sendAudio', description: 'Send audio', action: 'Send audio' },
|
|
28
|
+
{ name: 'Send Location', value: 'sendLocation', description: 'Send location', action: 'Send location' },
|
|
32
29
|
],
|
|
33
30
|
default: 'sendText',
|
|
34
31
|
},
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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',
|
|
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: 'Voice Note',
|
|
80
|
-
name: 'ptt',
|
|
81
|
-
type: 'boolean',
|
|
82
|
-
default: false,
|
|
83
|
-
displayOptions: { show: { operation: ['sendAudio'] } },
|
|
84
|
-
description: 'Send as voice note',
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
displayName: 'Latitude',
|
|
88
|
-
name: 'latitude',
|
|
89
|
-
type: 'number',
|
|
90
|
-
default: 0,
|
|
91
|
-
required: true,
|
|
92
|
-
displayOptions: { show: { operation: ['sendLocation'] } },
|
|
93
|
-
description: 'Location latitude',
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
displayName: 'Longitude',
|
|
97
|
-
name: 'longitude',
|
|
98
|
-
type: 'number',
|
|
99
|
-
default: 0,
|
|
100
|
-
required: true,
|
|
101
|
-
displayOptions: { show: { operation: ['sendLocation'] } },
|
|
102
|
-
description: 'Location longitude',
|
|
103
|
-
},
|
|
32
|
+
{ displayName: 'Recipient', name: 'recipient', type: 'string', default: '', required: true, description: 'Phone number or JID' },
|
|
33
|
+
{ displayName: 'Text', name: 'text', type: 'string', default: '', required: true, displayOptions: { show: { operation: ['sendText'] } }, description: 'Message text' },
|
|
34
|
+
{ displayName: 'Binary Property', name: 'binaryProperty', type: 'string', default: 'data', required: true, displayOptions: { show: { operation: ['sendImage', 'sendDocument', 'sendAudio'] } }, description: 'Binary property name' },
|
|
35
|
+
{ displayName: 'Caption', name: 'caption', type: 'string', default: '', displayOptions: { show: { operation: ['sendImage', 'sendDocument'] } }, description: 'Caption' },
|
|
36
|
+
{ displayName: 'File Name', name: 'fileName', type: 'string', default: 'file', required: true, displayOptions: { show: { operation: ['sendDocument'] } }, description: 'File name' },
|
|
37
|
+
{ displayName: 'Voice Note', name: 'ptt', type: 'boolean', default: false, displayOptions: { show: { operation: ['sendAudio'] } }, description: 'Voice note' },
|
|
38
|
+
{ displayName: 'Latitude', name: 'latitude', type: 'number', default: 0, required: true, displayOptions: { show: { operation: ['sendLocation'] } }, description: 'Latitude' },
|
|
39
|
+
{ displayName: 'Longitude', name: 'longitude', type: 'number', default: 0, required: true, displayOptions: { show: { operation: ['sendLocation'] } }, description: 'Longitude' },
|
|
104
40
|
],
|
|
105
41
|
};
|
|
106
42
|
}
|
|
@@ -109,60 +45,39 @@ class WhatsAppSend {
|
|
|
109
45
|
const returnData = [];
|
|
110
46
|
const credentials = await this.getCredentials('whatsappApi');
|
|
111
47
|
const cfg = await (0, WhatsAppApiHelper_1.getWhatsAppCredentials)(credentials);
|
|
112
|
-
// Ensure server is running
|
|
113
|
-
await (0, WhatsAppApiHelper_1.ensureServerRunning)(cfg);
|
|
114
|
-
const serverUrl = (0, WhatsAppApiHelper_1.getServerUrl)(cfg);
|
|
115
48
|
for (let i = 0; i < items.length; i++) {
|
|
116
49
|
try {
|
|
117
50
|
const operation = this.getNodeParameter('operation', i);
|
|
118
51
|
const recipient = this.getNodeParameter('recipient', i);
|
|
119
|
-
let
|
|
52
|
+
let content = {};
|
|
120
53
|
switch (operation) {
|
|
121
54
|
case 'sendText':
|
|
122
|
-
|
|
55
|
+
content = { text: this.getNodeParameter('text', i) };
|
|
123
56
|
break;
|
|
124
57
|
case 'sendImage': {
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
body.caption = this.getNodeParameter('caption', i) || undefined;
|
|
58
|
+
const bp = this.getNodeParameter('binaryProperty', i);
|
|
59
|
+
const buf = await this.helpers.getBinaryDataBuffer(i, bp);
|
|
60
|
+
content = { image: buf, caption: this.getNodeParameter('caption', i) || undefined };
|
|
129
61
|
break;
|
|
130
62
|
}
|
|
131
63
|
case 'sendDocument': {
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
body.fileName = this.getNodeParameter('fileName', i);
|
|
136
|
-
body.caption = this.getNodeParameter('caption', i) || undefined;
|
|
64
|
+
const bp = this.getNodeParameter('binaryProperty', i);
|
|
65
|
+
const buf = await this.helpers.getBinaryDataBuffer(i, bp);
|
|
66
|
+
content = { document: buf, fileName: this.getNodeParameter('fileName', i), caption: this.getNodeParameter('caption', i) || undefined };
|
|
137
67
|
break;
|
|
138
68
|
}
|
|
139
69
|
case 'sendAudio': {
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
body.ptt = this.getNodeParameter('ptt', i);
|
|
70
|
+
const bp = this.getNodeParameter('binaryProperty', i);
|
|
71
|
+
const buf = await this.helpers.getBinaryDataBuffer(i, bp);
|
|
72
|
+
content = { audio: buf, ptt: this.getNodeParameter('ptt', i) };
|
|
144
73
|
break;
|
|
145
74
|
}
|
|
146
75
|
case 'sendLocation':
|
|
147
|
-
|
|
148
|
-
degreesLatitude: this.getNodeParameter('latitude', i),
|
|
149
|
-
degreesLongitude: this.getNodeParameter('longitude', i),
|
|
150
|
-
};
|
|
76
|
+
content = { location: { degreesLatitude: this.getNodeParameter('latitude', i), degreesLongitude: this.getNodeParameter('longitude', i) } };
|
|
151
77
|
break;
|
|
152
78
|
}
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
headers: { 'Content-Type': 'application/json' },
|
|
156
|
-
body: JSON.stringify(body),
|
|
157
|
-
});
|
|
158
|
-
const result = await response.json();
|
|
159
|
-
if (!response.ok) {
|
|
160
|
-
throw new Error(result.error || 'Send failed');
|
|
161
|
-
}
|
|
162
|
-
returnData.push({
|
|
163
|
-
json: { ...result, operation, success: true },
|
|
164
|
-
pairedItem: { item: i },
|
|
165
|
-
});
|
|
79
|
+
const result = await (0, WhatsAppApiHelper_1.sendMessageWithAntiBan)(recipient, content, cfg);
|
|
80
|
+
returnData.push({ json: { ...result, operation, success: true }, pairedItem: { item: i } });
|
|
166
81
|
}
|
|
167
82
|
catch (error) {
|
|
168
83
|
if (this.continueOnFail()) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { INodeType, INodeTypeDescription,
|
|
1
|
+
import { INodeType, INodeTypeDescription, ITriggerFunctions, ITriggerResponse } from 'n8n-workflow';
|
|
2
2
|
export declare class WhatsAppTrigger implements INodeType {
|
|
3
3
|
description: INodeTypeDescription;
|
|
4
|
-
|
|
4
|
+
trigger(this: ITriggerFunctions): Promise<ITriggerResponse>;
|
|
5
5
|
}
|
|
@@ -11,12 +11,11 @@ class WhatsAppTrigger {
|
|
|
11
11
|
group: ['trigger'],
|
|
12
12
|
version: 1,
|
|
13
13
|
subtitle: '={{$parameter["event"]}}',
|
|
14
|
-
description: 'Triggers on incoming WhatsApp messages
|
|
14
|
+
description: 'Triggers on incoming WhatsApp messages — connection stays alive while workflow is active',
|
|
15
15
|
defaults: { name: 'WhatsApp Trigger' },
|
|
16
16
|
inputs: [],
|
|
17
17
|
outputs: ['main'],
|
|
18
18
|
credentials: [{ name: 'whatsappApi', required: true }],
|
|
19
|
-
polling: true,
|
|
20
19
|
properties: [
|
|
21
20
|
{
|
|
22
21
|
displayName: 'Event',
|
|
@@ -44,37 +43,71 @@ class WhatsAppTrigger {
|
|
|
44
43
|
],
|
|
45
44
|
};
|
|
46
45
|
}
|
|
47
|
-
async
|
|
46
|
+
async trigger() {
|
|
48
47
|
const credentials = await this.getCredentials('whatsappApi');
|
|
49
48
|
const cfg = await (0, WhatsAppApiHelper_1.getWhatsAppCredentials)(credentials);
|
|
50
49
|
const chatFilter = this.getNodeParameter('chatJidFilter', '');
|
|
51
50
|
const onlyText = this.getNodeParameter('onlyText', false);
|
|
52
|
-
//
|
|
53
|
-
await (0, WhatsAppApiHelper_1.
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
// Connect to WhatsApp — this keeps the socket alive
|
|
52
|
+
const sock = await (0, WhatsAppApiHelper_1.ensureConnected)(cfg);
|
|
53
|
+
// Listen for incoming messages
|
|
54
|
+
sock.ev.on('messages.upsert', (upsert) => {
|
|
55
|
+
const { messages, type } = upsert;
|
|
56
|
+
if (type !== 'notify')
|
|
57
|
+
return;
|
|
58
|
+
for (const msg of messages) {
|
|
59
|
+
if (msg.key.fromMe)
|
|
60
|
+
continue;
|
|
61
|
+
if (chatFilter && msg.key.remoteJid !== chatFilter)
|
|
62
|
+
continue;
|
|
63
|
+
const parsed = (0, WhatsAppApiHelper_1.parseIncomingMessage)(msg);
|
|
64
|
+
if (!parsed)
|
|
65
|
+
continue;
|
|
66
|
+
if (onlyText && parsed.messageType !== 'conversation' && parsed.messageType !== 'extendedTextMessage')
|
|
67
|
+
continue;
|
|
68
|
+
// Emit message to workflow
|
|
69
|
+
this.emit([[{ json: parsed }]]);
|
|
64
70
|
}
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
});
|
|
72
|
+
// Handle connection errors
|
|
73
|
+
sock.ev.on('connection.update', (update) => {
|
|
74
|
+
if (update.connection === 'close') {
|
|
75
|
+
this.emitError(new Error('WhatsApp connection closed'));
|
|
67
76
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
77
|
+
});
|
|
78
|
+
// Cleanup when workflow deactivates
|
|
79
|
+
let closeFunctionCalled = false;
|
|
80
|
+
const closeFunction = async () => {
|
|
81
|
+
closeFunctionCalled = true;
|
|
82
|
+
try {
|
|
83
|
+
sock.end(new Error('Workflow deactivated'));
|
|
84
|
+
}
|
|
85
|
+
catch { }
|
|
86
|
+
};
|
|
87
|
+
// Manual trigger for testing
|
|
88
|
+
const manualTriggerFunction = async () => {
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
const handler = (upsert) => {
|
|
91
|
+
for (const msg of upsert.messages) {
|
|
92
|
+
if (msg.key.fromMe)
|
|
93
|
+
continue;
|
|
94
|
+
const parsed = (0, WhatsAppApiHelper_1.parseIncomingMessage)(msg);
|
|
95
|
+
if (parsed) {
|
|
96
|
+
sock.ev.off('messages.upsert', handler);
|
|
97
|
+
this.emit([[{ json: parsed }]]);
|
|
98
|
+
resolve();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
sock.ev.on('messages.upsert', handler);
|
|
104
|
+
setTimeout(() => { sock.ev.off('messages.upsert', handler); resolve(); }, 30000);
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
return {
|
|
108
|
+
closeFunction,
|
|
109
|
+
manualTriggerFunction,
|
|
110
|
+
};
|
|
78
111
|
}
|
|
79
112
|
}
|
|
80
113
|
exports.WhatsAppTrigger = WhatsAppTrigger;
|
package/package.json
CHANGED