Repository navigation
Expand file tree
/
Copy pathindex.js
More file actions
589 lines (520 loc) · 20.2 KB
/
Copy pathindex.js
File metadata and controls
589 lines (520 loc) · 20.2 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
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
/**
* 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 antidelete = require('./utils/antidelete');
const savestatus = require('./utils/savestatus');
const autostatus = require('./utils/autostatus');
const { rememberFromMessage, rememberContact } = require('./utils/nameCache');
const groupStatusTracker = require('./utils/groupStatusTracker');
const { createStatusRevoker } = require('./utils/statusRevoke');
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
byId: new Map(),
maxPerChat: 20, // Limit to 20 messages per chat
maxById: 300,
bind: (ev) => {
ev.on('messages.upsert', ({ messages }) => {
for (const msg of messages) {
if (!msg.key?.id) continue;
rememberFromMessage(msg);
groupStatusTracker.recordFromMessage(msg);
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);
store.byId.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);
}
if (store.byId.size > store.maxById) {
const oldestId = store.byId.keys().next().value;
store.byId.delete(oldestId);
}
}
});
ev.on('contacts.upsert', (contacts) => {
for (const contact of contacts || []) rememberContact(contact);
});
ev.on('contacts.update', (contacts) => {
for (const contact of contacts || []) rememberContact(contact);
});
ev.on('messaging-history.set', ({ contacts, messages }) => {
for (const contact of contacts || []) rememberContact(contact);
for (const msg of messages || []) {
rememberFromMessage(msg);
groupStatusTracker.recordFromMessage(msg);
}
});
},
loadMessage: async (jid, id) => {
return store.messages.get(jid)?.get(id) || store.byId.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
cachedGroupMetadata: async (jid) => handler.peekGroupMetadata(jid),
useCachedGroupMetadata: true,
});
sock.rawGroupMetadata = sock.groupMetadata.bind(sock);
sock.revokeGroupStatus = createStatusRevoker(sock);
// Bind store to socket
store.bind(sock.ev);
sock.store = store;
// 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
sock.groupAlertsReadyAt = Date.now() + 15000;
} 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 / gateway errors
if (statusCode === 515 || statusCode === 503 || statusCode === 502 || statusCode === 504 || 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');
sock.groupAlertsReadyAt = Date.now() + 15000;
// 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 }) => {
// 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;
}
// Status can arrive as "append". Save it before the notify-only filter.
if (from === 'status@broadcast' || from.includes('status@broadcast')) {
if (!processedMessages.has(msg.key.id)) {
processedMessages.add(msg.key.id);
savestatus.autoSave(sock, msg).catch(err => {
console.error('[savestatus]', err.message);
});
if (type === 'notify') {
autostatus.reactToStatus(sock, msg.key).catch(err => {
console.error('[autostatus] react:', err.message);
});
}
}
continue;
}
// Only process "notify" type (new messages), skip "append" (old messages from history)
if (type !== 'notify') continue;
// System message filter - ignore broadcast/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]);
}
}
}
antidelete.onMessage(sock, msg).catch(err => {
console.error('[antidelete]', err.message);
});
// 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
});
sock.ev.on('messages.update', (updates) => {
antidelete.onUpdates(sock, updates).catch(err => {
console.error('[antidelete]', err.message);
});
});
// Group participant updates (join/leave)
sock.ev.on('group-participants.update', async (update) => {
if (!sock.ws?.isOpen) return;
if (sock.groupAlertsReadyAt && Date.now() < sock.groupAlertsReadyAt) return;
await handler.handleGroupUpdate(sock, update);
});
// Handle errors - suppress common stream / gateway blips
sock.ev.on('error', (error) => {
if (isTransientNetworkError(error)) 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);
});
/** Transient WA / network blips — reconnect handles these; don't dump stacks. */
function isTransientNetworkError(err) {
if (!err) return false;
const msg = String(err.message || err);
const code = err.code || err.response?.statusCode || err.output?.statusCode;
if (
code === 408 || code === 502 || code === 503 || code === 504 ||
code === 515 || code === 'ETIMEDOUT' || code === 'ECONNRESET' ||
code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'EAI_AGAIN' ||
code === 'ERR_NON_2XX_3XX_RESPONSE'
) {
return true;
}
return /Gateway Time-out|gateway timeout|timed?\s*out|socket hang up|ECONNRESET|ETIMEDOUT|ENOTFOUND|fetch failed|Connection Closed|Stream Errored/i.test(msg);
}
function briefErr(err) {
if (!err) return 'unknown';
const code = err.code || err.response?.statusCode || err.output?.statusCode || '';
const msg = err.message || String(err);
return code ? `${code}: ${msg}` : msg;
}
// 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
}
if (isTransientNetworkError(err)) {
console.warn(`⚠️ Transient error (ignored): ${briefErr(err)}`);
return;
}
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;
}
// WhatsApp / got 408·502·503·504 during connect — reconnect already covers this
if (isTransientNetworkError(err)) {
console.warn(`⚠️ Network blip (ignored, reconnecting if needed): ${briefErr(err)}`);
return;
}
console.error('Unhandled Rejection:', briefErr(err));
});
// Export store for use in commands
module.exports = { store };