-
-
Notifications
You must be signed in to change notification settings - Fork 720
Expand file tree
/
Copy pathindex.js
More file actions
501 lines (439 loc) · 16.7 KB
/
index.js
File metadata and controls
501 lines (439 loc) · 16.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
/**
* WhatsApp MD Bot - Main Entry Point
*/
process.env.PUPPETEER_SKIP_DOWNLOAD = 'true';
process.env.PUPPETEER_SKIP_CHROMIUM_DOWNLOAD = 'true';
process.env.PUPPETEER_CACHE_DIR = process.env.PUPPETEER_CACHE_DIR || '/tmp/puppeteer_cache_disabled';
const { initializeTempSystem } = require('./utils/tempManager');
const { startCleanup } = require('./utils/cleanup');
initializeTempSystem();
startCleanup();
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
const forbiddenPatternsConsole = [
'closing session',
'closing open session',
'sessionentry',
'prekey bundle',
'pendingprekey',
'_chains',
'registrationid',
'currentratchet',
'chainkey',
'ratchet',
'signal protocol',
'ephemeralkeypair',
'indexinfo',
'basekey'
];
console.log = (...args) => {
const message = args.map(a => typeof a === 'string' ? a : typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ').toLowerCase();
if (!forbiddenPatternsConsole.some(pattern => message.includes(pattern))) {
originalConsoleLog.apply(console, args);
}
};
console.error = (...args) => {
const message = args.map(a => typeof a === 'string' ? a : typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ').toLowerCase();
if (!forbiddenPatternsConsole.some(pattern => message.includes(pattern))) {
originalConsoleError.apply(console, args);
}
};
console.warn = (...args) => {
const message = args.map(a => typeof a === 'string' ? a : typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' ').toLowerCase();
if (!forbiddenPatternsConsole.some(pattern => message.includes(pattern))) {
originalConsoleWarn.apply(console, args);
}
};
// Now safe to load libraries
const pino = require('pino');
const {
default: makeWASocket,
useMultiFileAuthState,
DisconnectReason,
Browsers,
fetchLatestBaileysVersion
} = require('@whiskeysockets/baileys');
const qrcode = require('qrcode-terminal');
const config = require('./config');
const handler = require('./handler');
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const os = require('os');
// Remove Puppeteer cache (if some dependency downloaded Chromium into ~/.cache/puppeteer)
function cleanupPuppeteerCache() {
try {
const home = os.homedir();
const cacheDir = path.join(home, '.cache', 'puppeteer');
if (fs.existsSync(cacheDir)) {
console.log('🧹 Removing Puppeteer cache at:', cacheDir);
fs.rmSync(cacheDir, { recursive: true, force: true });
console.log('✅ Puppeteer cache removed');
}
} catch (err) {
console.error('⚠️ Failed to cleanup Puppeteer cache:', err.message || err);
}
}
// Optimized in-memory store with hard limits (Map-based for better memory management)
const store = {
messages: new Map(), // Use Map instead of plain object
maxPerChat: 20, // Limit to 20 messages per chat
bind: (ev) => {
ev.on('messages.upsert', ({ messages }) => {
for (const msg of messages) {
if (!msg.key?.id) continue;
const jid = msg.key.remoteJid;
if (!store.messages.has(jid)) {
store.messages.set(jid, new Map());
}
const chatMsgs = store.messages.get(jid);
chatMsgs.set(msg.key.id, msg);
// Aggressive cleanup per chat - keep only recent messages
if (chatMsgs.size > store.maxPerChat) {
// Remove oldest message (first entry in Map)
const oldestKey = chatMsgs.keys().next().value;
chatMsgs.delete(oldestKey);
}
}
});
},
loadMessage: async (jid, id) => {
return store.messages.get(jid)?.get(id) || null;
}
};
// Optimized message deduplication (Set-based, no timestamps needed)
const processedMessages = new Set();
// Aggressive cleanup - clear every 5 minutes
setInterval(() => {
processedMessages.clear();
}, 5 * 60 * 1000); // Every 5 minutes
// Custom Pino logger with suppression for Baileys noise
const createSuppressedLogger = (level = 'silent') => {
const forbiddenPatterns = [
'closing session',
'closing open session',
'sessionentry',
'prekey bundle',
'pendingprekey',
'_chains',
'registrationid',
'currentratchet',
'chainkey',
'ratchet',
'signal protocol',
'ephemeralkeypair',
'indexinfo',
'basekey',
'sessionentry',
'ratchetkey'
];
let logger;
try {
logger = pino({
level,
// Fallback transport without pino-pretty (in case not installed)
transport: process.env.NODE_ENV === 'production' ? undefined : {
target: 'pino-pretty',
options: {
colorize: true,
ignore: 'pid,hostname'
}
},
customLevels: {
trace: 0,
debug: 1,
info: 2,
warn: 3,
error: 4,
fatal: 5
},
// Redact sensitive fields
redact: ['registrationId', 'ephemeralKeyPair', 'rootKey', 'chainKey', 'baseKey']
});
} catch (err) {
// Fallback to basic pino without transport
logger = pino({ level });
}
// Wrap log methods to filter
const originalInfo = logger.info.bind(logger);
logger.info = (...args) => {
const msg = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ').toLowerCase();
if (!forbiddenPatterns.some(pattern => msg.includes(pattern))) {
originalInfo(...args);
}
};
logger.debug = () => { }; // Fully disable debug
logger.trace = () => { }; // Fully disable trace
return logger;
};
// Main connection function
async function startBot() {
const sessionFolder = `./${config.sessionName}`;
const sessionFile = path.join(sessionFolder, 'creds.json');
// Check if sessionID is provided and process KnightBot! format session
if (config.sessionID && config.sessionID.startsWith('KnightBot!')) {
try {
const [header, b64data] = config.sessionID.split('!');
if (header !== 'KnightBot' || !b64data) {
throw new Error("❌ Invalid session format. Expected 'KnightBot!.....'");
}
const cleanB64 = b64data.replace('...', '');
const compressedData = Buffer.from(cleanB64, 'base64');
const decompressedData = zlib.gunzipSync(compressedData);
// Ensure session folder exists
if (!fs.existsSync(sessionFolder)) {
fs.mkdirSync(sessionFolder, { recursive: true });
}
// Write decompressed session data to creds.json
fs.writeFileSync(sessionFile, decompressedData, 'utf8');
console.log('📡 Session : 🔑 Retrieved from KnightBot Session');
} catch (e) {
console.error('📡 Session : ❌ Error processing KnightBot session:', e.message);
// Continue with normal QR flow if session processing fails
}
}
const { state, saveCreds } = await useMultiFileAuthState(sessionFolder);
const { version } = await fetchLatestBaileysVersion();
// Use suppressed logger for socket
const suppressedLogger = createSuppressedLogger('silent');
const sock = makeWASocket({
version, // explicit WA Web version negotiated with the server
logger: suppressedLogger,
printQRInTerminal: false,
// Use a common desktop browser signature
browser: ['Chrome', 'Windows', '10.0'],
auth: state,
// Memory optimization: prevent loading old messages into RAM
syncFullHistory: false,
downloadHistory: false,
markOnlineOnConnect: false,
getMessage: async () => undefined // Don't load messages from store
});
// Bind store to socket
store.bind(sock.ev);
// Watchdog for inactive socket (Baileys bug fix)
let lastActivity = Date.now();
const INACTIVITY_TIMEOUT = 30 * 60 * 1000; // 30 minutes
// Update on every message
sock.ev.on('messages.upsert', () => {
lastActivity = Date.now();
});
// Check every 5 min
const watchdogInterval = setInterval(async () => {
if (Date.now() - lastActivity > INACTIVITY_TIMEOUT && sock.ws.readyState === 1) { // WebSocket open but inactive
console.log('⚠️ No activity detected. Forcing reconnect...');
await sock.end(undefined, undefined, { reason: 'inactive' });
clearInterval(watchdogInterval);
setTimeout(() => startBot(), 5000); // Slightly longer delay
}
}, 5 * 60 * 1000); // Every 5 min check
// Clear on close/open
sock.ev.on('connection.update', (update) => {
const { connection } = update;
if (connection === 'open') {
lastActivity = Date.now(); // Reset on open
} else if (connection === 'close') {
clearInterval(watchdogInterval);
}
});
// Connection update handler
sock.ev.on('connection.update', async (update) => {
const { connection, lastDisconnect, qr } = update;
if (qr) {
console.log('\n\n📱 Scan this QR code with WhatsApp:\n');
qrcode.generate(qr, { small: true });
}
if (connection === 'close') {
const shouldReconnect = lastDisconnect?.error?.output?.statusCode !== DisconnectReason.loggedOut;
const statusCode = lastDisconnect?.error?.output?.statusCode;
const errorMessage = lastDisconnect?.error?.message || 'Unknown error';
// Suppress verbose error output for common stream errors (515, etc.)
if (statusCode === 515 || statusCode === 503 || statusCode === 408) {
console.log(`⚠️ Connection closed (${statusCode}). Reconnecting...`);
} else {
console.log('Connection closed due to:', errorMessage, '\nReconnecting:', shouldReconnect);
}
if (shouldReconnect) {
setTimeout(() => startBot(), 3000);
}
} else if (connection === 'open') {
console.log('\n✅ Bot connected successfully!');
console.log(`📱 Bot Number: ${sock.user.id.split(':')[0]}`);
console.log(`🤖 Bot Name: ${config.botName}`);
console.log(`⚡ Prefix: ${config.prefix}`);
const ownerNames = Array.isArray(config.ownerName) ? config.ownerName.join(',') : config.ownerName;
console.log(`👑 Owner: ${ownerNames}\n`);
console.log('Bot is ready to receive messages!\n');
// Set bot status
if (config.autoBio) {
await sock.updateProfileStatus(`${config.botName} | Active 24/7`);
}
// Initialize anti-call feature
handler.initializeAntiCall(sock);
// Cleanup old chats (keep only active ones, e.g., last touched <1 day)
const now = Date.now();
for (const [jid, chatMsgs] of store.messages.entries()) {
const timestamps = Array.from(chatMsgs.values()).map(m => m.messageTimestamp * 1000 || 0);
if (timestamps.length > 0 && now - Math.max(...timestamps) > 24 * 60 * 60 * 1000) { // 1 day old chat
store.messages.delete(jid);
}
}
console.log(`🧹 Store cleaned. Active chats: ${store.messages.size}`);
}
});
// Credentials update handler
sock.ev.on('creds.update', saveCreds);
// System JID filter - checks if JID is from broadcast/status/newsletter
const isSystemJid = (jid) => {
if (!jid) return true;
return jid.includes('@broadcast') ||
jid.includes('status.broadcast') ||
jid.includes('@newsletter') ||
jid.includes('@newsletter.');
};
// Messages handler - Process only new messages
sock.ev.on('messages.upsert', ({ messages, type }) => {
// Only process "notify" type (new messages), skip "append" (old messages from history)
if (type !== 'notify') return;
// Process messages in the array
for (const msg of messages) {
// Skip if message is invalid or missing key
if (!msg.message || !msg.key?.id) continue;
const from = msg.key.remoteJid;
if (!from) {
continue;
}
// System message filter - ignore broadcast/status/newsletter messages
if (isSystemJid(from)) {
continue; // Silently ignore system messages
}
// Deduplication: Skip if message has already been processed
const msgId = msg.key.id;
if (processedMessages.has(msgId)) continue;
// Timestamp validation: Only process messages within last 5 minutes
const MESSAGE_AGE_LIMIT = 5 * 60 * 1000; // 5 minutes in milliseconds
let messageAge = 0;
if (msg.messageTimestamp) {
messageAge = Date.now() - (msg.messageTimestamp * 1000);
if (messageAge > MESSAGE_AGE_LIMIT) {
// Message is too old, skip processing
continue;
}
}
// Mark message as processed
processedMessages.add(msgId);
// Store message FIRST (before processing)
// from already defined above in DM block check
if (msg.key && msg.key.id) {
if (!store.messages.has(from)) {
store.messages.set(from, new Map());
}
const chatMsgs = store.messages.get(from);
chatMsgs.set(msg.key.id, msg);
// Cleanup: Keep only last 20 per chat (reduced from 200)
if (chatMsgs.size > store.maxPerChat) {
// Remove oldest messages
const sortedIds = Array.from(chatMsgs.entries())
.sort((a, b) => (a[1].messageTimestamp || 0) - (b[1].messageTimestamp || 0))
.map(([id]) => id);
for (let i = 0; i < sortedIds.length - store.maxPerChat; i++) {
chatMsgs.delete(sortedIds[i]);
}
}
}
// Process command IMMEDIATELY (don't block on other operations)
handler.handleMessage(sock, msg).catch(err => {
if (!err.message?.includes('rate-overlimit') &&
!err.message?.includes('not-authorized')) {
console.error('Error handling message:', err.message);
}
});
// Do other operations in background (non-blocking)
setImmediate(async () => {
if (config.autoRead && from.endsWith('@g.us')) {
try {
await sock.readMessages([msg.key]);
} catch (e) {
// Silently handle
}
}
if (from.endsWith('@g.us')) {
try {
const groupMetadata = await handler.getGroupMetadata(sock, msg.key.remoteJid);
if (groupMetadata) {
await handler.handleAntilink(sock, msg, groupMetadata);
}
} catch (error) {
// Silently handle
}
}
});
}
});
// Message receipt updates (silently handled, no logging)
sock.ev.on('message-receipt.update', () => {
// Silently handle receipt updates
});
// Message updates (silently handled, no logging)
sock.ev.on('messages.update', () => {
// Silently handle message updates
});
// Group participant updates (join/leave)
sock.ev.on('group-participants.update', async (update) => {
await handler.handleGroupUpdate(sock, update);
});
// Handle errors - suppress common stream errors
sock.ev.on('error', (error) => {
const statusCode = error?.output?.statusCode;
// Suppress verbose output for common stream errors
if (statusCode === 515 || statusCode === 503 || statusCode === 408) {
// These are usually temporary connection issues, handled by reconnection
return;
}
console.error('Socket error:', error.message || error);
});
return sock;
}
// Start the bot
console.log('🚀 Starting WhatsApp MD Bot...\n');
console.log(`📦 Bot Name: ${config.botName}`);
console.log(`⚡ Prefix: ${config.prefix}`);
const ownerNames = Array.isArray(config.ownerName) ? config.ownerName.join(',') : config.ownerName;
console.log(`👑 Owner: ${ownerNames}\n`);
// Proactively delete Puppeteer cache so it doesn't fill disk on panels
cleanupPuppeteerCache();
startBot().catch(err => {
console.error('Error starting bot:', err);
process.exit(1);
});
// Handle process termination
process.on('uncaughtException', (err) => {
// Handle ENOSPC errors gracefully without crashing
if (err.code === 'ENOSPC' || err.errno === -28 || err.message?.includes('no space left on device')) {
console.error('⚠️ ENOSPC Error: No space left on device. Attempting cleanup...');
const { cleanupOldFiles } = require('./utils/cleanup');
cleanupOldFiles();
console.warn('⚠️ Cleanup completed. Bot will continue but may experience issues until space is freed.');
return; // Don't crash, just log and continue
}
console.error('Uncaught Exception:', err);
});
process.on('unhandledRejection', (err) => {
// Handle ENOSPC errors gracefully
if (err.code === 'ENOSPC' || err.errno === -28 || err.message?.includes('no space left on device')) {
console.warn('⚠️ ENOSPC Error in promise: No space left on device. Attempting cleanup...');
const { cleanupOldFiles } = require('./utils/cleanup');
cleanupOldFiles();
console.warn('⚠️ Cleanup completed. Bot will continue but may experience issues until space is freed.');
return; // Don't crash, just log and continue
}
// Don't spam console with rate limit errors
if (err.message && err.message.includes('rate-overlimit')) {
console.warn('⚠️ Rate limit reached. Please slow down your requests.');
return;
}
console.error('Unhandled Rejection:', err);
});
// Export store for use in commands
module.exports = { store };