const express = require('express'); const fs = require('fs-extra'); const path = require('path'); const os = require('os'); const { exec } = require('child_process'); const router = express.Router(); const pino = require('pino'); const moment = require('moment-timezone'); const Jimp = require('jimp'); const crypto = require('crypto'); const axios = require('axios'); const FileType = require('file-type'); const fetch = require('node-fetch'); const { MongoClient } = require('mongodb'); const ffmpeg = require('fluent-ffmpeg'); const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg'); // Set the path for fluent-ffmpeg to find the ffmpeg executable ffmpeg.setFfmpegPath(ffmpegInstaller.path); const { default: makeWASocket, useMultiFileAuthState, delay, getContentType, makeCacheableSignalKeyStore, Browsers, jidNormalizedUser, downloadContentFromMessage, DisconnectReason } = require('baileyz'); const { title } = require('process'); // ---------------- CONFIG ---------------- const BOT_NAME_FANCY = 'FREE HOSTING BOT'; const config = { MONGO_URI: process.env.MONGO_URI || 'mongodb+srv://chamafreehosting:Chamindu47@21@freechama.moe3veb.mongodb.net//', SESSION_ID: process.env.SESSION_ID || '', // your session id or catbox url CREDS_JSON: process.env.CREDS_JSON || '', // your raw creds.json content AUTO_VIEW_STATUS: 'true', AUTO_LIKE_STATUS: 'true', AUTO_RECORDING: 'false', AUTO_LIKE_EMOJI: ['๐Ÿ’™', '๐Ÿฉท', '๐Ÿ’œ', '๐ŸคŽ', '๐Ÿงก', '๐Ÿฉต', '๐Ÿ’›', '๐Ÿฉถ', 'โ™ฅ๏ธ', '๐Ÿ’—', 'โค๏ธโ€๐Ÿ”ฅ'], PREFIX: '.', MAX_RETRIES: 3, GROUP_INVITE_LINK: 'https://chat.whatsapp.com/xxxxxxxxxxxxxxxxxx', RCD_IMAGE_PATH: 'https://i.ibb.co/ZpMz03zx/A-striking-high-quality-202603302119.jpg', NEWSLETTER_JID: '1201234567890@newsletter', OTP_EXPIRY: 300000, OWNER_NUMBER: process.env.OWNER_NUMBER || '94783314361', CHANNEL_LINK: 'https://whatsapp.com/channel/xxxxxxxxxxxxxxxxxx', BOT_NAME: 'FREE HOSTING BOT', BOT_VERSION: '1.0.0V', OWNER_NAME: 'CHAMINDU RANSIKA', IMAGE_PATH: 'https://i.ibb.co/ZpMz03zx/A-striking-high-quality-202603302119.jpg', BOT_FOOTER: '> *FREE HOSTING BOT*', BUTTON_IMAGES: { ALIVE: 'https://i.ibb.co/ZpMz03zx/A-striking-high-quality-202603302119.jpg' } }; // ---------------- MONGO SETUP ---------------- const MONGO_URI = config.MONGO_URI; const MONGO_DB = process.env.MONGO_DB || 'FREE'; let mongoClient, mongoDB; let sessionsCol, numbersCol, adminsCol, newsletterCol, configsCol, newsletterReactsCol; async function initMongo() { try { if (mongoClient && mongoClient.topology && mongoClient.topology.isConnected && mongoClient.topology.isConnected()) return; } catch (e) { } mongoClient = new MongoClient(MONGO_URI, { useNewUrlParser: true, useUnifiedTopology: true }); await mongoClient.connect(); mongoDB = mongoClient.db(MONGO_DB); sessionsCol = mongoDB.collection('sessions'); numbersCol = mongoDB.collection('numbers'); adminsCol = mongoDB.collection('admins'); newsletterCol = mongoDB.collection('newsletter_list'); configsCol = mongoDB.collection('configs'); newsletterReactsCol = mongoDB.collection('newsletter_reacts'); await sessionsCol.createIndex({ number: 1 }, { unique: true }); await numbersCol.createIndex({ number: 1 }, { unique: true }); await newsletterCol.createIndex({ jid: 1 }, { unique: true }); await newsletterReactsCol.createIndex({ jid: 1 }, { unique: true }); await configsCol.createIndex({ number: 1 }, { unique: true }); console.log('โœ… Mongo initialized and collections ready'); } // ---------------- Mongo helpers ---------------- async function saveCredsToMongo(number, creds, keys = null) { try { await initMongo(); const sanitized = number.replace(/[^0-9]/g, ''); const doc = { number: sanitized, creds, keys, updatedAt: new Date() }; await sessionsCol.updateOne({ number: sanitized }, { $set: doc }, { upsert: true }); console.log(`Saved creds to Mongo for ${sanitized}`); } catch (e) { console.error('saveCredsToMongo error:', e); } } async function loadCredsFromMongo(number) { try { await initMongo(); const sanitized = number.replace(/[^0-9]/g, ''); const doc = await sessionsCol.findOne({ number: sanitized }); return doc || null; } catch (e) { console.error('loadCredsFromMongo error:', e); return null; } } async function removeSessionFromMongo(number) { try { await initMongo(); const sanitized = number.replace(/[^0-9]/g, ''); await sessionsCol.deleteOne({ number: sanitized }); console.log(`Removed session from Mongo for ${sanitized}`); } catch (e) { console.error('removeSessionToMongo error:', e); } } async function addNumberToMongo(number) { try { await initMongo(); const sanitized = number.replace(/[^0-9]/g, ''); await numbersCol.updateOne({ number: sanitized }, { $set: { number: sanitized } }, { upsert: true }); console.log(`Added number ${sanitized} to Mongo numbers`); } catch (e) { console.error('addNumberToMongo', e); } } async function removeNumberFromMongo(number) { try { await initMongo(); const sanitized = number.replace(/[^0-9]/g, ''); await numbersCol.deleteOne({ number: sanitized }); console.log(`Removed number ${sanitized} from Mongo numbers`); } catch (e) { console.error('removeNumberFromMongo', e); } } async function getAllNumbersFromMongo() { try { await initMongo(); const docs = await numbersCol.find({}).toArray(); return docs.map(d => d.number); } catch (e) { console.error('getAllNumbersFromMongo', e); return []; } } async function loadAdminsFromMongo() { try { await initMongo(); const docs = await adminsCol.find({}).toArray(); return docs.map(d => d.jid || d.number).filter(Boolean); } catch (e) { console.error('loadAdminsFromMongo', e); return []; } } async function addAdminToMongo(jidOrNumber) { try { await initMongo(); const doc = { jid: jidOrNumber }; await adminsCol.updateOne({ jid: jidOrNumber }, { $set: doc }, { upsert: true }); console.log(`Added admin ${jidOrNumber}`); } catch (e) { console.error('addAdminToMongo', e); } } async function removeAdminFromMongo(jidOrNumber) { try { await initMongo(); await adminsCol.deleteOne({ jid: jidOrNumber }); console.log(`Removed admin ${jidOrNumber}`); } catch (e) { console.error('removeAdminFromMongo', e); } } async function addNewsletterToMongo(jid, emojis = []) { try { await initMongo(); const doc = { jid, emojis: Array.isArray(emojis) ? emojis : [], addedAt: new Date() }; await newsletterCol.updateOne({ jid }, { $set: doc }, { upsert: true }); console.log(`Added newsletter ${jid} -> emojis: ${doc.emojis.join(',')}`); } catch (e) { console.error('addNewsletterToMongo', e); throw e; } } async function removeNewsletterFromMongo(jid) { try { await initMongo(); await newsletterCol.deleteOne({ jid }); console.log(`Removed newsletter ${jid}`); } catch (e) { console.error('removeNewsletterFromMongo', e); throw e; } } async function listNewslettersFromMongo() { try { await initMongo(); const docs = await newsletterCol.find({}).toArray(); return docs.map(d => ({ jid: d.jid, emojis: Array.isArray(d.emojis) ? d.emojis : [] })); } catch (e) { console.error('listNewslettersFromMongo', e); return []; } } async function saveNewsletterReaction(jid, messageId, emoji, sessionNumber) { try { await initMongo(); const doc = { jid, messageId, emoji, sessionNumber, ts: new Date() }; if (!mongoDB) await initMongo(); const col = mongoDB.collection('newsletter_reactions_log'); await col.insertOne(doc); console.log(`Saved reaction ${emoji} for ${jid}#${messageId}`); } catch (e) { console.error('saveNewsletterReaction', e); } } async function setUserConfigInMongo(number, conf) { try { await initMongo(); const sanitized = number.replace(/[^0-9]/g, ''); await configsCol.updateOne({ number: sanitized }, { $set: { number: sanitized, config: conf, updatedAt: new Date() } }, { upsert: true }); } catch (e) { console.error('setUserConfigInMongo', e); } } async function loadUserConfigFromMongo(number) { try { await initMongo(); const sanitized = number.replace(/[^0-9]/g, ''); const doc = await configsCol.findOne({ number: sanitized }); return doc ? doc.config : null; } catch (e) { console.error('loadUserConfigFromMongo', e); return null; } } // -------------- newsletter react-config helpers -------------- async function addNewsletterReactConfig(jid, emojis = []) { try { await initMongo(); await newsletterReactsCol.updateOne({ jid }, { $set: { jid, emojis, addedAt: new Date() } }, { upsert: true }); console.log(`Added react-config for ${jid} -> ${emojis.join(',')}`); } catch (e) { console.error('addNewsletterReactConfig', e); throw e; } } async function removeNewsletterReactConfig(jid) { try { await initMongo(); await newsletterReactsCol.deleteOne({ jid }); console.log(`Removed react-config for ${jid}`); } catch (e) { console.error('removeNewsletterReactConfig', e); throw e; } } async function listNewsletterReactsFromMongo() { try { await initMongo(); const docs = await newsletterReactsCol.find({}).toArray(); return docs.map(d => ({ jid: d.jid, emojis: Array.isArray(d.emojis) ? d.emojis : [] })); } catch (e) { console.error('listNewsletterReactsFromMongo', e); return []; } } async function getReactConfigForJid(jid) { try { await initMongo(); const doc = await newsletterReactsCol.findOne({ jid }); return doc ? (Array.isArray(doc.emojis) ? doc.emojis : []) : null; } catch (e) { console.error('getReactConfigForJid', e); return null; } } // ---------------- basic utils ---------------- function formatMessage(title, content, footer) { return `${title}\n\n${content}\n\n> *${footer}*`; } function generateOTP() { return Math.floor(100000 + Math.random() * 900000).toString(); } function getSriLankaTimestamp() { return moment().tz('Asia/Colombo').format('YYYY-MM-DD HH:mm:ss'); } const activeSockets = new Map(); const socketCreationTime = new Map(); const otpStore = new Map(); // ---------------- helpers kept/adapted ---------------- async function joinGroup(socket) { let retries = config.MAX_RETRIES; const inviteCodeMatch = (config.GROUP_INVITE_LINK || '').match(/chat\.whatsapp\.com\/([a-zA-Z0-9]+)/); if (!inviteCodeMatch) return { status: 'failed', error: 'No group invite configured' }; const inviteCode = inviteCodeMatch[1]; while (retries > 0) { try { const response = await socket.groupAcceptInvite(inviteCode); if (response?.gid) return { status: 'success', gid: response.gid }; throw new Error('No group ID in response'); } catch (error) { retries--; let errorMessage = error.message || 'Unknown error'; if (error.message && error.message.includes('not-authorized')) errorMessage = 'Bot not authorized'; else if (error.message && error.message.includes('conflict')) errorMessage = 'Already a member'; else if (error.message && error.message.includes('gone')) errorMessage = 'Invite invalid/expired'; if (retries === 0) return { status: 'failed', error: errorMessage }; await delay(2000 * (config.MAX_RETRIES - retries)); } } return { status: 'failed', error: 'Max retries reached' }; } async function sendAdminConnectMessage(socket, number, groupResult, sessionConfig = {}) { const admins = await loadAdminsFromMongo(); const groupStatus = groupResult.status === 'success' ? `Joined (ID: ${groupResult.gid})` : `Failed to join group: ${groupResult.error}`; const botName = sessionConfig.botName || BOT_NAME_FANCY; const image = config.RCD_IMAGE_PATH; const caption = formatMessage(botName, `*๐Ÿ“ž ๐—กแดœแดส™แด‡ส€:* ${number}\n*๐Ÿ ๐—ฆแด›แด€แด›แดœ๊œฑ:* ${groupStatus}\n*๐Ÿ•’ ๐—–แดษดษดแด‡แด„แด›แด‡แด… ๐—”แด›:* ${getSriLankaTimestamp()}`, botName); for (const admin of admins) { try { const to = admin.includes('@') ? admin : `${admin}@s.whatsapp.net`; if (String(image).startsWith('http')) { await socket.sendMessage(to, { image: { url: image }, caption }); } else { try { const buf = fs.readFileSync(image); await socket.sendMessage(to, { image: buf, caption }); } catch (e) { await socket.sendMessage(to, { image: { url: config.RCD_IMAGE_PATH }, caption }); } } } catch (err) { console.error('Failed to send connect message to admin', admin, err?.message || err); } } } async function sendOwnerConnectMessage(socket, number, groupResult, sessionConfig = {}) { try { const ownerJid = `${config.OWNER_NUMBER.replace(/[^0-9]/g, '')}@s.whatsapp.net`; const activeCount = activeSockets.size; const botName = sessionConfig.botName || BOT_NAME_FANCY; const image = config.RCD_IMAGE_PATH; const groupStatus = groupResult.status === 'success' ? `Joined (ID: ${groupResult.gid})` : `Failed to join group: ${groupResult.error}`; const caption = formatMessage(`*๐Ÿฅท ๐—ขแดกษดแด‡ส€ ๐—–แดษดแด›แด€แด„แด›: ${config.OWNER_NAME}*`, `*๐Ÿ“ž ๐—กแดœแดส™แด‡ส€:* ${number}\n*๐Ÿ ๐—ฆแด›แด€แด›แดœ๊œฑ:* ${groupStatus}\n*๐Ÿ•’ ๐—–แดษดษดแด‡แด„แด›แด‡แด… ๐—”แด›:* ${getSriLankaTimestamp()}\n\n*๐Ÿ”ข ๐—”แด„แด›ษชแด แด‡ ๐—ฆแด‡๊œฑ๊œฑษชแดษด๊œฑ:* ${activeCount}`, botName); if (String(image).startsWith('http')) { await socket.sendMessage(ownerJid, { image: { url: image }, caption }); } else { try { const buf = fs.readFileSync(image); await socket.sendMessage(ownerJid, { image: buf, caption }); } catch (e) { await socket.sendMessage(ownerJid, { image: { url: config.RCD_IMAGE_PATH }, caption }); } } } catch (err) { console.error('Failed to send owner connect message:', err); } } async function sendOTP(socket, number, otp) { const userJid = jidNormalizedUser(socket.user.id); const message = formatMessage(`*๐Ÿ” ๐Ž๐šƒ๐™ฟ ๐•๐™ด๐š๐™ธ๐™ต๐™ธ๐™ฒ๐™ฐ๐šƒ๐™ธ๐™พ๐™ฝ โ€” ${BOT_NAME_FANCY}*`, `*๐˜๐™พ๐š„๐š ๐Ž๐šƒ๐™ฟ ๐…๐™พ๐š ๐‚๐™พ๐™ฝ๐™ต๐™ธ๐™ถ ๐”๐™ฟ๐™ณ๐™ฐ๐šƒ๐™ด ๐ˆ๐š‚:* *${otp}*\n๐“๐™ท๐™ธ๐š‚ ๐Ž๐šƒ๐™ฟ ๐–๐™ธ๐™ป๐™ป ๐„๐š‡๐™ฟ๐™ธ๐š๐™ด ๐ˆ๐™ฝ 5 ๐Œ๐™ธ๐™ฝ๐š„๐šƒ๐™ด๐š‚.\n\n*๐๐š„๐™ผ๐™ฑ๐™ด๐š:* ${number}`, BOT_NAME_FANCY); try { await socket.sendMessage(userJid, { text: message }); console.log(`OTP ${otp} sent to ${number}`); } catch (error) { console.error(`Failed to send OTP to ${number}:`, error); throw error; } } // ---------------- handlers (newsletter + reactions) ---------------- async function setupNewsletterHandlers(socket, sessionNumber) { const rrPointers = new Map(); socket.ev.on('messages.upsert', async ({ messages }) => { const message = messages[0]; if (!message?.key) return; const jid = message.key.remoteJid; try { const followedDocs = await listNewslettersFromMongo(); // array of {jid, emojis} const reactConfigs = await listNewsletterReactsFromMongo(); // [{jid, emojis}] const reactMap = new Map(); for (const r of reactConfigs) reactMap.set(r.jid, r.emojis || []); const followedJids = followedDocs.map(d => d.jid); if (!followedJids.includes(jid) && !reactMap.has(jid)) return; let emojis = reactMap.get(jid) || null; if ((!emojis || emojis.length === 0) && followedDocs.find(d => d.jid === jid)) { emojis = (followedDocs.find(d => d.jid === jid).emojis || []); } if (!emojis || emojis.length === 0) emojis = config.AUTO_LIKE_EMOJI; let idx = rrPointers.get(jid) || 0; const emoji = emojis[idx % emojis.length]; rrPointers.set(jid, (idx + 1) % emojis.length); const messageId = message.newsletterServerId || message.key.id; if (!messageId) return; let retries = 3; while (retries-- > 0) { try { if (typeof socket.newsletterReactMessage === 'function') { await socket.newsletterReactMessage(jid, messageId.toString(), emoji); } else { await socket.sendMessage(jid, { react: { text: emoji, key: message.key } }); } console.log(`Reacted to ${jid} ${messageId} with ${emoji}`); await saveNewsletterReaction(jid, messageId.toString(), emoji, sessionNumber || null); break; } catch (err) { console.warn(`Reaction attempt failed (${3 - retries}/3):`, err?.message || err); await delay(1200); } } } catch (error) { console.error('Newsletter reaction handler error:', error?.message || error); } }); } // ---------------- status + revocation + resizing ---------------- async function setupStatusHandlers(socket, sessionNumber) { socket.ev.on('messages.upsert', async ({ messages }) => { const message = messages[0]; if (!message?.key || message.key.remoteJid !== 'status@broadcast' || !message.key.participant) return; try { // Load user-specific config from MongoDB let userEmojis = config.AUTO_LIKE_EMOJI; // Default emojis let autoViewStatus = config.AUTO_VIEW_STATUS; // Default from global config let autoLikeStatus = config.AUTO_LIKE_STATUS; // Default from global config let autoRecording = config.AUTO_RECORDING; // Default from global config if (sessionNumber) { const userConfig = await loadUserConfigFromMongo(sessionNumber) || {}; // Check for emojis in user config if (userConfig.AUTO_LIKE_EMOJI && Array.isArray(userConfig.AUTO_LIKE_EMOJI) && userConfig.AUTO_LIKE_EMOJI.length > 0) { userEmojis = userConfig.AUTO_LIKE_EMOJI; } // Check for auto view status in user config if (userConfig.AUTO_VIEW_STATUS !== undefined) { autoViewStatus = userConfig.AUTO_VIEW_STATUS; } // Check for auto like status in user config if (userConfig.AUTO_LIKE_STATUS !== undefined) { autoLikeStatus = userConfig.AUTO_LIKE_STATUS; } // Check for auto recording in user config if (userConfig.AUTO_RECORDING !== undefined) { autoRecording = userConfig.AUTO_RECORDING; } } // Use auto recording setting (from user config or global) if (autoRecording === 'true') { await socket.sendPresenceUpdate("recording", message.key.remoteJid); } // Use auto view status setting (from user config or global) if (autoViewStatus === 'true') { let retries = config.MAX_RETRIES; while (retries > 0) { try { await socket.readMessages([message.key]); break; } catch (error) { retries--; await delay(1000 * (config.MAX_RETRIES - retries)); if (retries === 0) throw error; } } } // Use auto like status setting (from user config or global) if (autoLikeStatus === 'true') { const randomEmoji = userEmojis[Math.floor(Math.random() * userEmojis.length)]; let retries = config.MAX_RETRIES; while (retries > 0) { try { await socket.sendMessage(message.key.remoteJid, { react: { text: randomEmoji, key: message.key } }, { statusJidList: [message.key.participant] }); break; } catch (error) { retries--; await delay(1000 * (config.MAX_RETRIES - retries)); if (retries === 0) throw error; } } } } catch (error) { console.error('Status handler error:', error); } }); } async function handleMessageRevocation(socket, number) { socket.ev.on('messages.delete', async ({ keys }) => { if (!keys || keys.length === 0) return; const messageKey = keys[0]; const userJid = jidNormalizedUser(socket.user.id); const deletionTime = getSriLankaTimestamp(); const message = formatMessage('*๐Ÿ—‘๏ธ ๐— แด‡๊œฑ๊œฑแด€ษขแด‡ ๐——แด‡สŸแด‡แด›แด‡แด…*', `A message was deleted from your chat.\n*๐Ÿ“‹ ๐—™ส€แดแด:* ${messageKey.remoteJid}\n*๐Ÿ ๐——แด‡สŸแด‡แด›ษชแดษด ๐—งษชแดแด‡:* ${deletionTime}`, BOT_NAME_FANCY); try { await socket.sendMessage(userJid, { image: { url: config.RCD_IMAGE_PATH }, caption: message }); } catch (error) { console.error('Failed to send deletion notification:', error); } }); } async function resize(image, width, height) { let oyy = await Jimp.read(image); return await oyy.resize(width, height).getBufferAsync(Jimp.MIME_JPEG); } // ---------------- command handlers ---------------- function setupCommandHandlers(socket, number) { socket.ev.on('messages.upsert', async ({ messages }) => { const msg = messages[0]; if (!msg || !msg.message || msg.key.remoteJid === 'status@broadcast' || msg.key.remoteJid === config.NEWSLETTER_JID) return; const type = getContentType(msg.message); if (!msg.message) return; msg.message = (getContentType(msg.message) === 'ephemeralMessage') ? msg.message.ephemeralMessage.message : msg.message; const from = msg.key.remoteJid; const sender = from; const nowsender = msg.key.fromMe ? (socket.user.id.split(':')[0] + '@s.whatsapp.net' || socket.user.id) : (msg.key.participant || msg.key.remoteJid); const senderNumber = (nowsender || '').split('@')[0]; const developers = `${config.OWNER_NUMBER}`; const botNumber = socket.user.id.split(':')[0]; const isbot = botNumber.includes(senderNumber); const isOwner = isbot ? isbot : developers.includes(senderNumber); const isGroup = from.endsWith("@g.us"); let body = (type === 'conversation') ? msg.message.conversation : msg.message?.extendedTextMessage?.contextInfo?.hasOwnProperty('quotedMessage') ? msg.message.extendedTextMessage.text : (type == 'interactiveResponseMessage') ? msg.message.interactiveResponseMessage?.nativeFlowResponseMessage && JSON.parse(msg.message.interactiveResponseMessage.nativeFlowResponseMessage.paramsJson)?.id : (type == 'templateButtonReplyMessage') ? msg.message.templateButtonReplyMessage?.selectedId : (type === 'extendedTextMessage') ? msg.message.extendedTextMessage.text : (type == 'imageMessage') && msg.message.imageMessage.caption ? msg.message.imageMessage.caption : (type == 'videoMessage') && msg.message.videoMessage.caption ? msg.message.videoMessage.caption : (type == 'buttonsResponseMessage') ? msg.message.buttonsResponseMessage?.selectedButtonId : (type == 'listResponseMessage') ? msg.message.listResponseMessage?.singleSelectReply?.selectedRowId : (type == 'messageContextInfo') ? (msg.message.buttonsResponseMessage?.selectedButtonId || msg.message.listResponseMessage?.singleSelectReply?.selectedRowId || msg.text) : (type === 'viewOnceMessage') ? msg.message[type]?.message[getContentType(msg.message[type].message)] : (type === "viewOnceMessageV2") ? (msg.msg.message.imageMessage?.caption || msg.msg.message.videoMessage?.caption || "") : ''; body = String(body || ''); if (!body || typeof body !== 'string') return; const prefix = config.PREFIX; const isCmd = body && body.startsWith && body.startsWith(prefix); const command = isCmd ? body.slice(prefix.length).trim().split(' ').shift().toLowerCase() : null; const args = body.trim().split(/ +/).slice(1); // helper: download quoted media into buffer async function downloadQuotedMedia(quoted) { if (!quoted) return null; const qTypes = ['imageMessage', 'videoMessage', 'audioMessage', 'documentMessage', 'stickerMessage']; const qType = qTypes.find(t => quoted[t]); if (!qType) return null; const messageType = qType.replace(/Message$/i, '').toLowerCase(); const stream = await downloadContentFromMessage(quoted[qType], messageType); let buffer = Buffer.from([]); for await (const chunk of stream) buffer = Buffer.concat([buffer, chunk]); return { buffer, mime: quoted[qType].mimetype || '', caption: quoted[qType].caption || quoted[qType].fileName || '', ptt: quoted[qType].ptt || false, fileName: quoted[qType].fileName || '' }; } if (!command) return; try { // Load user config for work type restrictions const sanitized = (number || '').replace(/[^0-9]/g, ''); const userConfig = await loadUserConfigFromMongo(sanitized) || {}; // ========== ADD WORK TYPE RESTRICTIONS HERE ========== // Apply work type restrictions for non-owner users if (!isOwner) { // Get work type from user config or fallback to global config const workType = userConfig.WORK_TYPE || 'public'; // Default to public if not set // If work type is "private", only owner can use commands if (workType === "private") { console.log(`Command blocked: WORK_TYPE is private for ${sanitized}`); return; } // If work type is "inbox", block commands in groups if (isGroup && workType === "inbox") { console.log(`Command blocked: WORK_TYPE is inbox but message is from group for ${sanitized}`); return; } // If work type is "groups", block commands in private chats if (!isGroup && workType === "groups") { console.log(`Command blocked: WORK_TYPE is groups but message is from private chat for ${sanitized}`); return; } // If work type is "public", allow all (no restrictions needed) } // ========== END WORK TYPE RESTRICTIONS ========== switch (command) { case 'alive': { try { const sanitized = (number || '').replace(/[^0-9]/g, ''); const cfg = await loadUserConfigFromMongo(sanitized) || {}; const botName = cfg.botName || BOT_NAME_FANCY; const logo = cfg.logo || config.RCD_IMAGE_PATH; // Get current time for Sri Lanka (IST - UTC+5:30) const now = new Date(); // Set Sri Lanka timezone const options = { timeZone: 'Asia/Colombo' }; // Get current hour in Sri Lanka time const sriLankaTime = now.toLocaleString('en-US', { timeZone: 'Asia/Colombo' }); const sriLankaDate = new Date(sriLankaTime); const currentHour = sriLankaDate.getHours(); let greeting; if (currentHour >= 5 && currentHour < 12) { greeting = 'Good Morning ๐ŸŒ…'; } else if (currentHour >= 12 && currentHour < 18) { greeting = 'Good Afternoon'; } else { greeting = 'Good Evening ๐ŸŒ™'; } // Format date and day separately for Sri Lanka const optionsDate = { month: 'long', day: 'numeric', timeZone: 'Asia/Colombo' }; const formattedDate = sriLankaDate.toLocaleDateString('en-US', optionsDate); const optionsDay = { weekday: 'long', timeZone: 'Asia/Colombo' }; const formattedDay = sriLankaDate.toLocaleDateString('en-US', optionsDay); // Format time for Sri Lanka const optionsTime = { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true, timeZone: 'Asia/Colombo' }; const formattedTime = sriLankaDate.toLocaleTimeString('en-US', optionsTime); // Meta AI mention const metaQuote = { key: { remoteJid: "status@broadcast", participant: "0@s.whatsapp.net", fromMe: false, id: "META_AI_ALIVE" }, message: { contactMessage: { displayName: botName, vcard: `BEGIN:VCARD\nVERSION:3.0\nN:${botName};;;;\nFN:${botName}\nORG:Meta Platforms\nTEL;type=CELL;type=VOICE;waid=13135550002:+1 313 555 0002\nEND:VCARD` } } }; // 1. Send video note first const vnoteUrl = 'https://files.catbox.moe/dityqg.mp4'; await socket.sendMessage(sender, { video: { url: vnoteUrl }, ptv: true }, { quoted: metaQuote }); await new Promise(resolve => setTimeout(resolve, 500)); // 2. Then send alive message const startTime = socketCreationTime.get(number) || Date.now(); const uptime = Math.floor((Date.now() - startTime) / 1000); const hours = Math.floor(uptime / 3600); const minutes = Math.floor((uptime % 3600) / 60); const seconds = Math.floor(uptime % 60); const text = ` *๐—›ษช ๐Ÿ‘‹ ๐— ษชษดษช ๐—•แดแด› ๐—จ๊œฑแด‡ส€* *โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ* *โ”ƒ๐Ÿ—ฏ๏ธ ๐—šส€แด‡แด‡แด›ษชษดษข :* ${greeting} *โ”ƒ๐Ÿ—“๏ธ ๐——แด€แด›แด‡ :* ${formattedDate} *โ”ƒ๐Ÿ“† ๐——แด€ส :* ${formattedDay} *โ”ƒโฑ๏ธ ๐—งษชแดแด‡ :* ${formattedTime} (IST) *โ”ƒ๐Ÿ“„ ๐—•แดแด› ๐—กแด€แดแด‡ :* ${config.BOT_NAME} *โ”ƒ๐Ÿฅท ๐—ขแดกษดแด‡ส€ :* ${config.OWNER_NAME} *โ”ƒ๐Ÿงฌ ๐—ฉแด‡ส€๊œฑษชแดษด :* ${config.BOT_VERSION} *โ”ƒ๐ŸŽˆ ๐—ฃสŸแด€แด›๊œฐแดส€แด :* ${process.env.PLATFORM || 'Hugging Face'} *โ”ƒ๐Ÿ“Ÿ ๐—จแด˜แด›ษชแดแด‡ :* ${hours}h ${minutes}m ${seconds}s *โ”ƒโœ’๏ธ ๐—ฃส€แด‡๊œฐษชx :* ${config.PREFIX} *โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ* `; const buttons = [ { buttonId: 'action', buttonText: { displayText: 'Menu Options' }, type: 4, nativeFlowInfo: { name: 'single_select', paramsJson: JSON.stringify({ title: '๐—–สŸษชแด„แด‹ ๐—›แด‡ส€แด‡', sections: [ { title: `CHAMA OFC`, highlight_label: '', rows: [ { title: 'Alive', description: 'Get Bot Speed', id: `${config.PREFIX}alive`, }, { title: 'Ping', description: 'Get Bot Speed', id: `${config.PREFIX}ping`, }, ], }, ], }), }, }, ] let imagePayload = String(logo).startsWith('http') ? { url: logo } : fs.readFileSync(logo); await socket.sendMessage(sender, { image: imagePayload, caption: text, footer: ` *${botName}*`, buttons, headerType: 4 }, { quoted: metaQuote }); } catch (e) { console.error('alive error', e); await socket.sendMessage(sender, { text: 'โŒ Failed to send alive status.' }, { quoted: msg }); } break; } case 'ping': { try { const start = Date.now(); const sanitized = (number || '').replace(/[^0-9]/g, ''); const cfg = await loadUserConfigFromMongo(sanitized) || {}; const botName = cfg.botName || BOT_NAME_FANCY; const logo = cfg.logo || config.RCD_IMAGE_PATH; const userTag = `@${sender.split("@")[0]} `; // Sri Lanka Time const now = new Date(); const sriLankaTime = now.toLocaleString('en-US', { timeZone: 'Asia/Colombo' }); const sriLankaDate = new Date(sriLankaTime); const currentHour = sriLankaDate.getHours(); let greeting; if (currentHour >= 5 && currentHour < 12) { greeting = 'Good Morning ๐ŸŒ…'; } else if (currentHour >= 12 && currentHour < 18) { greeting = 'Good Afternoon โ˜€๏ธ'; } else { greeting = 'Good Evening ๐ŸŒ™'; } const formattedTime = sriLankaDate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true, timeZone: 'Asia/Colombo' }); // Runtime const uptime = process.uptime(); const hours = Math.floor(uptime / 3600); const minutes = Math.floor((uptime % 3600) / 60); const seconds = Math.floor(uptime % 60); const end = Date.now(); const latency = end - start; const speedStatus = latency < 200 ? 'Excellent ๐ŸŸข' : latency < 500 ? 'Good ๐ŸŸก' : 'Slow ๐Ÿ”ด'; const text = ` ๐Ÿ“ ๐—ฃ๐—ข๐—ก๐—š ๐—ฅ๐—˜๐—ฆ๐—จ๐—Ÿ๐—ง ๐Ÿ‘ค USER: ${userTag} ๐Ÿ—ฏ๏ธ GREETING: ${greeting} โฐ TIME: ${formattedTime} โšก SPEED: ${latency} ms ๐Ÿ–ฅ๏ธ RUNTIME: ${hours}h ${minutes}m ${seconds} s ๐Ÿ“ก STATUS: ${speedStatus} Thanks for using ${botName} ๐Ÿš€ `; let imagePayload = String(logo).startsWith('http') ? { url: logo } : fs.readFileSync(logo); // ๐Ÿ”˜ Buttons const buttons = [ { buttonId: 'menu', buttonText: { displayText: 'โฌ…๏ธ Back To Menu' }, type: 1 }, { buttonId: 'alive', buttonText: { displayText: '๐Ÿค– Alive' }, type: 1 } ]; await socket.sendMessage(sender, { image: imagePayload, caption: text, footer: `* ${botName} * `, buttons: buttons, headerType: 4 }, { quoted: msg }); } catch (e) { console.error('ping error', e); await socket.sendMessage(sender, { text: 'โŒ Failed to test ping.' }, { quoted: msg }); } break; } case 'system': { try { const sanitized = (number || '').replace(/[^0-9]/g, ''); const cfg = await loadUserConfigFromMongo(sanitized) || {}; const botName = cfg.botName || BOT_NAME_FANCY; const logo = cfg.logo || config.RCD_IMAGE_PATH; const metaQuote = { key: { remoteJid: "status@broadcast", participant: "0@s.whatsapp.net", fromMe: false, id: "META_AI_SYSTEM" }, message: { contactMessage: { displayName: botName, vcard: `BEGIN:VCARD\nVERSION: 3.0\nN:${botName};;;; \nFN:${botName} \nORG:Meta Platforms\nTEL; type = CELL; type = VOICE; waid = 13135550002: +1 313 555 0002\nEND: VCARD` } } }; const os = require('os'); const text = ` *โ˜˜๏ธ system info for ${botName} โ˜˜๏ธ* *โ•ญโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ—†* *โ”ƒ๐Ÿงธ o๊œฑ:* ${os.type()} ${os.release()} *โ”ƒ๐Ÿ“ก pสŸแด€แด›๊œฐแดส€แด:* ${os.platform()} *โ”ƒ๐Ÿง  cแด˜แดœ แด„แดส€แด‡๊œฑ:* ${os.cpus().length} *โ”ƒ๐Ÿ’พ Mแด‡แดแดส€ส:* ${(os.totalmem() / 1024 / 1024 / 1024).toFixed(2)} GB *โ•ฐโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ—†* `; let imagePayload = String(logo).startsWith('http') ? { url: logo } : fs.readFileSync(logo); await socket.sendMessage(sender, { image: imagePayload, caption: text, footer: `* ${botName} ๐—ฆส๊œฑแด›แด‡แด ๐—œษด๊œฐแด * `, buttons: [{ buttonId: `${config.PREFIX} menu`, buttonText: { displayText: "๐Ÿ“„ ๐— แด‡ษดแดœ" }, type: 1 }], headerType: 4 }, { quoted: metaQuote }); } catch (e) { console.error('system error', e); await socket.sendMessage(sender, { text: 'โŒ Failed to get system info.' }, { quoted: msg }); } break; } } } catch (err) { console.error('Command handler error:', err); try { await socket.sendMessage(sender, { image: { url: config.RCD_IMAGE_PATH }, caption: formatMessage('โŒ ERROR', 'An error occurred while processing your command. Please try again.', BOT_NAME_FANCY) }); } catch (e) { } } }); } // ---------------- Call Rejection Handler ---------------- // ---------------- Simple Call Rejection Handler ---------------- async function setupCallRejection(socket, sessionNumber) { socket.ev.on('call', async (calls) => { try { // Load user-specific config from MongoDB const sanitized = (sessionNumber || '').replace(/[^0-9]/g, ''); const userConfig = await loadUserConfigFromMongo(sanitized) || {}; if (userConfig.ANTI_CALL !== 'on') return; console.log(`๐Ÿ“ž Incoming call detected for ${sanitized} - Auto rejecting...`); for (const call of calls) { if (call.status !== 'offer') continue; const id = call.id; const from = call.from; // Reject the call await socket.rejectCall(id, from); // Send rejection message to caller await socket.sendMessage(from, { text: '*๐Ÿ”• Auto call rejection is enabled. Calls are automatically rejected.*' }); console.log(`โœ… Auto - rejected call from ${from}`); // Send notification to bot user const userJid = jidNormalizedUser(socket.user.id); const rejectionMessage = formatMessage( '๐Ÿ“ž CALL REJECTED', `Auto call rejection is active.\n\nCall from: ${from}\nTime: ${getSriLankaTimestamp()}`, BOT_NAME_FANCY ); await socket.sendMessage(userJid, { image: { url: config.RCD_IMAGE_PATH }, caption: rejectionMessage }); } } catch (err) { console.error(`Call rejection error for ${sessionNumber}: `, err); } }); } // ---------------- Auto Message Read Handler ---------------- async function setupAutoMessageRead(socket, sessionNumber) { socket.ev.on('messages.upsert', async ({ messages }) => { const msg = messages[0]; if (!msg || !msg.message || msg.key.remoteJid === 'status@broadcast' || msg.key.remoteJid === config.NEWSLETTER_JID) return; // Quick return if no need to process const sanitized = (sessionNumber || '').replace(/[^0-9]/g, ''); const userConfig = await loadUserConfigFromMongo(sanitized) || {}; const autoReadSetting = userConfig.AUTO_READ_MESSAGE || 'off'; if (autoReadSetting === 'off') return; const from = msg.key.remoteJid; // Simple message body extraction let body = ''; try { const type = getContentType(msg.message); const actualMsg = (type === 'ephemeralMessage') ? msg.message.ephemeralMessage.message : msg.message; if (type === 'conversation') { body = actualMsg.conversation || ''; } else if (type === 'extendedTextMessage') { body = actualMsg.extendedTextMessage?.text || ''; } else if (type === 'imageMessage') { body = actualMsg.imageMessage?.caption || ''; } else if (type === 'videoMessage') { body = actualMsg.videoMessage?.caption || ''; } } catch (e) { // If we can't extract body, treat as non-command body = ''; } // Check if it's a command message const prefix = userConfig.PREFIX || config.PREFIX; const isCmd = body && body.startsWith && body.startsWith(prefix); // Apply auto read rules - SINGLE ATTEMPT ONLY if (autoReadSetting === 'all') { // Read all messages - one attempt only try { await socket.readMessages([msg.key]); console.log(`โœ… Message read: ${msg.key.id} `); } catch (error) { console.warn('Failed to read message (single attempt):', error?.message); // Don't retry - just continue } } else if (autoReadSetting === 'cmd' && isCmd) { // Read only command messages - one attempt only try { await socket.readMessages([msg.key]); console.log(`โœ… Command message read: ${msg.key.id} `); } catch (error) { console.warn('Failed to read command message (single attempt):', error?.message); // Don't retry - just continue } } }); } // ---------------- message handlers ---------------- function setupMessageHandlers(socket, sessionNumber) { socket.ev.on('messages.upsert', async ({ messages }) => { const msg = messages[0]; if (!msg.message || msg.key.remoteJid === 'status@broadcast' || msg.key.remoteJid === config.NEWSLETTER_JID) return; try { // Load user-specific config from MongoDB let autoTyping = config.AUTO_TYPING; // Default from global config let autoRecording = config.AUTO_RECORDING; // Default from global config if (sessionNumber) { const userConfig = await loadUserConfigFromMongo(sessionNumber) || {}; // Check for auto typing in user config if (userConfig.AUTO_TYPING !== undefined) { autoTyping = userConfig.AUTO_TYPING; } // Check for auto recording in user config if (userConfig.AUTO_RECORDING !== undefined) { autoRecording = userConfig.AUTO_RECORDING; } } // Use auto typing setting (from user config or global) if (autoTyping === 'true') { try { await socket.sendPresenceUpdate('composing', msg.key.remoteJid); // Stop typing after 3 seconds setTimeout(async () => { try { await socket.sendPresenceUpdate('paused', msg.key.remoteJid); } catch (e) { } }, 3000); } catch (e) { console.error('Auto typing error:', e); } } // Use auto recording setting (from user config or global) if (autoRecording === 'true') { try { await socket.sendPresenceUpdate('recording', msg.key.remoteJid); // Stop recording after 3 seconds setTimeout(async () => { try { await socket.sendPresenceUpdate('paused', msg.key.remoteJid); } catch (e) { } }, 3000); } catch (e) { console.error('Auto recording error:', e); } } } catch (error) { console.error('Message handler error:', error); } }); } // ---------------- cleanup helper ---------------- async function deleteSessionAndCleanup(number, socketInstance) { const sanitized = number.replace(/[^0-9]/g, ''); try { const sessionPath = path.join(os.tmpdir(), `session_${sanitized} `); try { if (fs.existsSync(sessionPath)) fs.removeSync(sessionPath); } catch (e) { } activeSockets.delete(sanitized); socketCreationTime.delete(sanitized); try { await removeSessionFromMongo(sanitized); } catch (e) { } try { await removeNumberFromMongo(sanitized); } catch (e) { } try { const ownerJid = `${config.OWNER_NUMBER.replace(/[^0-9]/g, '')} @s.whatsapp.net`; const caption = formatMessage('*๐Ÿฅท OWNER NOTICE โ€” SESSION REMOVED*', `* ๐umber:* ${sanitized} \n * ๐’ession ๐‘emoved ๐ƒue ๐“o ๐‹ogout.*\n\n * ๐€ctive ๐’essions ๐ow:* ${activeSockets.size} `, BOT_NAME_FANCY); if (socketInstance && socketInstance.sendMessage) await socketInstance.sendMessage(ownerJid, { image: { url: config.RCD_IMAGE_PATH }, caption }); } catch (e) { } console.log(`Cleanup completed for ${sanitized}`); } catch (err) { console.error('deleteSessionAndCleanup error:', err); } } // ---------------- auto-restart ---------------- function setupAutoRestart(socket, number) { socket.ev.on('connection.update', async (update) => { const { connection, lastDisconnect } = update; if (connection === 'close') { const statusCode = lastDisconnect?.error?.output?.statusCode || lastDisconnect?.error?.statusCode || (lastDisconnect?.error && lastDisconnect.error.toString().includes('401') ? 401 : undefined); const isLoggedOut = statusCode === 401 || (lastDisconnect?.error && lastDisconnect.error.code === 'AUTHENTICATION') || (lastDisconnect?.error && String(lastDisconnect.error).toLowerCase().includes('logged out')) || (lastDisconnect?.reason === DisconnectReason?.loggedOut); if (isLoggedOut) { console.log(`User ${number} logged out.Cleaning up...`); try { await deleteSessionAndCleanup(number, socket); } catch (e) { console.error(e); } } else { console.log(`Connection closed for ${number}(not logout).Attempt reconnect...`); try { await delay(10000); activeSockets.delete(number.replace(/[^0-9]/g, '')); socketCreationTime.delete(number.replace(/[^0-9]/g, '')); const mockRes = { headersSent: false, send: () => { }, status: () => mockRes }; await EmpirePair(number, mockRes); } catch (e) { console.error('Reconnect attempt failed', e); } } } }); } // ---------------- EmpirePair (pairing, temp dir, persist to Mongo) ---------------- // ---------------- EmpirePair (pairing, temp dir, persist to Mongo) ---------------- async function EmpirePair(number, res) { const sanitizedNumber = number.replace(/[^0-9]/g, ''); const sessionPath = path.join(os.tmpdir(), `session_${sanitizedNumber} `); await initMongo().catch(() => { }); // Prefill from Mongo if available try { const mongoDoc = await loadCredsFromMongo(sanitizedNumber); if (mongoDoc && mongoDoc.creds) { fs.ensureDirSync(sessionPath); fs.writeFileSync(path.join(sessionPath, 'creds.json'), JSON.stringify(mongoDoc.creds, null, 2)); if (mongoDoc.keys) fs.writeFileSync(path.join(sessionPath, 'keys.json'), JSON.stringify(mongoDoc.keys, null, 2)); console.log('Prefilled creds from Mongo'); } } catch (e) { console.warn('Prefill from Mongo failed', e); } const { state, saveCreds } = await useMultiFileAuthState(sessionPath); const logger = pino({ level: 'silent' }); try { const socket = makeWASocket({ logger: pino({ level: "silent" }), printQRInTerminal: false, auth: state, version: [2, 3000, 1033105955], connectTimeoutMs: 60000, defaultQueryTimeoutMs: 0, keepAliveIntervalMs: 10000, emitOwnEvents: true, fireInitQueries: true, generateHighQualityLinkPreview: true, syncFullHistory: true, markOnlineOnConnect: true, browser: ['Mac OS', 'Safari', '10.15.7'] }); socketCreationTime.set(sanitizedNumber, Date.now()); setupStatusHandlers(socket, sanitizedNumber); setupCommandHandlers(socket, sanitizedNumber); setupMessageHandlers(socket, sanitizedNumber); setupAutoRestart(socket, sanitizedNumber); setupNewsletterHandlers(socket, sanitizedNumber); handleMessageRevocation(socket, sanitizedNumber); setupAutoMessageRead(socket, sanitizedNumber); setupCallRejection(socket, sanitizedNumber); if (!socket.authState.creds.registered) { let retries = config.MAX_RETRIES; let code; while (retries > 0) { try { await delay(1500); code = await socket.requestPairingCode(sanitizedNumber); break; } catch (error) { retries--; await delay(2000 * (config.MAX_RETRIES - retries)); } } if (!res.headersSent) res.send({ code }); } // Save creds to Mongo when updated socket.ev.on('creds.update', async () => { try { await saveCreds(); const credsPath = path.join(sessionPath, 'creds.json'); if (!fs.existsSync(credsPath)) return; const fileStats = fs.statSync(credsPath); if (fileStats.size === 0) return; const fileContent = await fs.readFile(credsPath, 'utf8'); const trimmedContent = fileContent.trim(); if (!trimmedContent || trimmedContent === '{}' || trimmedContent === 'null') return; let credsObj; try { credsObj = JSON.parse(trimmedContent); } catch (e) { return; } if (!credsObj || typeof credsObj !== 'object') return; const keysObj = state.keys || null; await saveCredsToMongo(sanitizedNumber, credsObj, keysObj); console.log('โœ… Creds saved to MongoDB successfully'); } catch (err) { console.error('Failed saving creds on creds.update:', err); } }); socket.ev.on('connection.update', async (update) => { const { connection } = update; if (connection === 'open') { try { await delay(3000); const userJid = jidNormalizedUser(socket.user.id); const groupResult = await joinGroup(socket).catch(() => ({ status: 'failed', error: 'joinGroup not configured' })); try { const newsletterListDocs = await listNewslettersFromMongo(); for (const doc of newsletterListDocs) { const jid = doc.jid; try { if (typeof socket.newsletterFollow === 'function') await socket.newsletterFollow(jid); } catch (e) { } } } catch (e) { } activeSockets.set(sanitizedNumber, socket); const groupStatus = groupResult.status === 'success' ? 'Joined successfully' : `Failed to join group: ${groupResult.error} `; const userConfig = await loadUserConfigFromMongo(sanitizedNumber) || {}; const useBotName = userConfig.botName || BOT_NAME_FANCY; const useLogo = config.RCD_IMAGE_PATH; const initialCaption = formatMessage(useBotName, `*โœ… ๐—ฆแดœแด„แด„แด‡๊œฑ๊œฑ๊œฐแดœสŸสŸส ๐—–แดษดษดแด‡แด„แด›แด‡แด… โœ…*\n\n *๐Ÿ”ข ๐—กแดœแดส™แด‡ส€:* ${sanitizedNumber} \n *๐Ÿ“ก ๐—–แดษดษดแด‡แด„แด›ษชษดษข:* Wait few seconds`, useBotName ); let sentMsg = null; try { if (String(useLogo).startsWith('http')) { sentMsg = await socket.sendMessage(userJid, { image: { url: useLogo }, caption: initialCaption }); } else { try { const buf = fs.readFileSync(useLogo); sentMsg = await socket.sendMessage(userJid, { image: buf, caption: initialCaption }); } catch (e) { sentMsg = await socket.sendMessage(userJid, { image: { url: config.RCD_IMAGE_PATH }, caption: initialCaption }); } } } catch (e) { try { sentMsg = await socket.sendMessage(userJid, { text: initialCaption }); } catch (e) { } } await delay(4000); const updatedCaption = formatMessage(useBotName, `*โœ… ๐—ฆแดœแด„แด„แด‡๊œฑ๊œฑ๊œฐแดœสŸสŸส ๐—–แดษดษดแด‡แด„แด›แด‡แด… โœ…*\n\n *๐Ÿ”ข ๐—กแดœแดส™แด‡ส€:* ${sanitizedNumber} \n *๐Ÿท๏ธ ๐—ฆแด›แด€แด›แดœ๊œฑ:* ${groupStatus} \n *๐Ÿ•’ ๐—–แดษดษดแด‡แด„แด› ๐—งษชแดแด‡:* ${getSriLankaTimestamp()} `, useBotName ); try { if (sentMsg && sentMsg.key) { try { await socket.sendMessage(userJid, { delete: sentMsg.key }); } catch (delErr) { } } try { if (String(useLogo).startsWith('http')) { await socket.sendMessage(userJid, { image: { url: useLogo }, caption: updatedCaption }); } else { try { const buf = fs.readFileSync(useLogo); await socket.sendMessage(userJid, { image: buf, caption: updatedCaption }); } catch (e) { await socket.sendMessage(userJid, { text: updatedCaption }); } } } catch (imgErr) { await socket.sendMessage(userJid, { text: updatedCaption }); } } catch (e) { } await sendAdminConnectMessage(socket, sanitizedNumber, groupResult, userConfig); await sendOwnerConnectMessage(socket, sanitizedNumber, groupResult, userConfig); await addNumberToMongo(sanitizedNumber); } catch (e) { console.error('Connection open error:', e); try { exec(`pm2.restart ${process.env.PM2_NAME || 'CHATUWA-MINI-main'} `); } catch (e) { } } } if (connection === 'close') { try { if (fs.existsSync(sessionPath)) fs.removeSync(sessionPath); } catch (e) { } } }); activeSockets.set(sanitizedNumber, socket); } catch (error) { console.error('Pairing error:', error); socketCreationTime.delete(sanitizedNumber); if (!res.headersSent) res.status(503).send({ error: 'Service Unavailable' }); } } // ---------------- endpoints (admin/newsletter management + others) ---------------- router.post('/newsletter/add', async (req, res) => { const { jid, emojis } = req.body; if (!jid) return res.status(400).send({ error: 'jid required' }); if (!jid.endsWith('@newsletter')) return res.status(400).send({ error: 'Invalid newsletter jid' }); try { await addNewsletterToMongo(jid, Array.isArray(emojis) ? emojis : []); res.status(200).send({ status: 'ok', jid }); } catch (e) { res.status(500).send({ error: e.message || e }); } }); router.post('/newsletter/remove', async (req, res) => { const { jid } = req.body; if (!jid) return res.status(400).send({ error: 'jid required' }); try { await removeNewsletterFromMongo(jid); res.status(200).send({ status: 'ok', jid }); } catch (e) { res.status(500).send({ error: e.message || e }); } }); router.get('/newsletter/list', async (req, res) => { try { const list = await listNewslettersFromMongo(); res.status(200).send({ status: 'ok', channels: list }); } catch (e) { res.status(500).send({ error: e.message || e }); } }); // admin endpoints router.post('/admin/add', async (req, res) => { const { jid } = req.body; if (!jid) return res.status(400).send({ error: 'jid required' }); try { await addAdminToMongo(jid); res.status(200).send({ status: 'ok', jid }); } catch (e) { res.status(500).send({ error: e.message || e }); } }); router.post('/admin/remove', async (req, res) => { const { jid } = req.body; if (!jid) return res.status(400).send({ error: 'jid required' }); try { await removeAdminFromMongo(jid); res.status(200).send({ status: 'ok', jid }); } catch (e) { res.status(500).send({ error: e.message || e }); } }); router.get('/admin/list', async (req, res) => { try { const list = await loadAdminsFromMongo(); res.status(200).send({ status: 'ok', admins: list }); } catch (e) { res.status(500).send({ error: e.message || e }); } }); // existing endpoints (connect, reconnect, active, etc.) router.get('/', async (req, res) => { const { number } = req.query; if (!number) return res.status(400).send({ error: 'Number parameter is required' }); if (activeSockets.has(number.replace(/[^0-9]/g, ''))) return res.status(200).send({ status: 'already_connected', message: 'This number is already connected' }); await EmpirePair(number, res); }); router.get('/active', (req, res) => { res.status(200).send({ botName: BOT_NAME_FANCY, count: activeSockets.size, numbers: Array.from(activeSockets.keys()), timestamp: getSriLankaTimestamp() }); }); router.get('/ping', (req, res) => { res.status(200).send({ status: 'active', botName: BOT_NAME_FANCY, message: '๐™ท๐™ธ๐š๐š„ ๐š‡ ๐™ผ๐™ณ ๐™ผ๐™ธ๐™ฝ๐™ธ ๐™ฑ๐™พ๐šƒ', activesession: activeSockets.size }); }); router.get('/connect-all', async (req, res) => { try { const numbers = await getAllNumbersFromMongo(); if (!numbers || numbers.length === 0) return res.status(404).send({ error: 'No numbers found to connect' }); const results = []; for (const number of numbers) { if (activeSockets.has(number)) { results.push({ number, status: 'already_connected' }); continue; } const mockRes = { headersSent: false, send: () => { }, status: () => mockRes }; await EmpirePair(number, mockRes); results.push({ number, status: 'connection_initiated' }); } res.status(200).send({ status: 'success', connections: results }); } catch (error) { console.error('Connect all error:', error); res.status(500).send({ error: 'Failed to connect all bots' }); } }); router.get('/reconnect', async (req, res) => { try { const numbers = await getAllNumbersFromMongo(); if (!numbers || numbers.length === 0) return res.status(404).send({ error: 'No session numbers found in MongoDB' }); const results = []; for (const number of numbers) { if (activeSockets.has(number)) { results.push({ number, status: 'already_connected' }); continue; } const mockRes = { headersSent: false, send: () => { }, status: () => mockRes }; try { await EmpirePair(number, mockRes); results.push({ number, status: 'connection_initiated' }); } catch (err) { results.push({ number, status: 'failed', error: err.message }); } await delay(1000); } res.status(200).send({ status: 'success', connections: results }); } catch (error) { console.error('Reconnect error:', error); res.status(500).send({ error: 'Failed to reconnect bots' }); } }); router.get('/update-config', async (req, res) => { const { number, config: configString } = req.query; if (!number || !configString) return res.status(400).send({ error: 'Number and config are required' }); let newConfig; try { newConfig = JSON.parse(configString); } catch (error) { return res.status(400).send({ error: 'Invalid config format' }); } const sanitizedNumber = number.replace(/[^0-9]/g, ''); const socket = activeSockets.get(sanitizedNumber); if (!socket) return res.status(404).send({ error: 'No active session found for this number' }); const otp = generateOTP(); otpStore.set(sanitizedNumber, { otp, expiry: Date.now() + config.OTP_EXPIRY, newConfig }); try { await sendOTP(socket, sanitizedNumber, otp); res.status(200).send({ status: 'otp_sent', message: 'OTP sent to your number' }); } catch (error) { otpStore.delete(sanitizedNumber); res.status(500).send({ error: 'Failed to send OTP' }); } }); router.get('/verify-otp', async (req, res) => { const { number, otp } = req.query; if (!number || !otp) return res.status(400).send({ error: 'Number and OTP are required' }); const sanitizedNumber = number.replace(/[^0-9]/g, ''); const storedData = otpStore.get(sanitizedNumber); if (!storedData) return res.status(400).send({ error: 'No OTP request found for this number' }); if (Date.now() >= storedData.expiry) { otpStore.delete(sanitizedNumber); return res.status(400).send({ error: 'OTP has expired' }); } if (storedData.otp !== otp) return res.status(400).send({ error: 'Invalid OTP' }); try { await setUserConfigInMongo(sanitizedNumber, storedData.newConfig); otpStore.delete(sanitizedNumber); const sock = activeSockets.get(sanitizedNumber); if (sock) await sock.sendMessage(jidNormalizedUser(sock.user.id), { image: { url: config.RCD_IMAGE_PATH }, caption: formatMessage('๐Ÿ“Œ CONFIG UPDATED', 'Your configuration has been successfully updated!', BOT_NAME_FANCY) }); res.status(200).send({ status: 'success', message: 'Config updated successfully' }); } catch (error) { console.error('Failed to update config:', error); res.status(500).send({ error: 'Failed to update config' }); } }); router.get('/getabout', async (req, res) => { const { number, target } = req.query; if (!number || !target) return res.status(400).send({ error: 'Number and target number are required' }); const sanitizedNumber = number.replace(/[^0-9]/g, ''); const socket = activeSockets.get(sanitizedNumber); if (!socket) return res.status(404).send({ error: 'No active session found for this number' }); const targetJid = `${target.replace(/[^0-9]/g, '')} @s.whatsapp.net`; try { const statusData = await socket.fetchStatus(targetJid); const aboutStatus = statusData.status || 'No status available'; const setAt = statusData.setAt ? moment(statusData.setAt).tz('Asia/Colombo').format('YYYY-MM-DD HH:mm:ss') : 'Unknown'; res.status(200).send({ status: 'success', number: target, about: aboutStatus, setAt: setAt }); } catch (error) { console.error(`Failed to fetch status for ${target}: `, error); res.status(500).send({ status: 'error', message: `Failed to fetch About status for ${target}.` }); } }); // ---------------- Dashboard endpoints & static ---------------- const dashboardStaticDir = path.join(__dirname, 'dashboard_static'); if (!fs.existsSync(dashboardStaticDir)) fs.ensureDirSync(dashboardStaticDir); router.use('/dashboard/static', express.static(dashboardStaticDir)); router.get('/dashboard', async (req, res) => { res.sendFile(path.join(dashboardStaticDir, 'index.html')); }); // API: sessions & active & delete router.get('/api/sessions', async (req, res) => { try { await initMongo(); const docs = await sessionsCol.find({}, { projection: { number: 1, updatedAt: 1 } }).sort({ updatedAt: -1 }).toArray(); res.json({ ok: true, sessions: docs }); } catch (err) { console.error('API /api/sessions error', err); res.status(500).json({ ok: false, error: err.message || err }); } }); router.get('/api/active', async (req, res) => { try { const keys = Array.from(activeSockets.keys()); res.json({ ok: true, active: keys, count: keys.length }); } catch (err) { res.status(500).json({ ok: false, error: err.message || err }); } }); router.post('/api/session/delete', async (req, res) => { try { const { number } = req.body; if (!number) return res.status(400).json({ ok: false, error: 'number required' }); const sanitized = ('' + number).replace(/[^0-9]/g, ''); const running = activeSockets.get(sanitized); if (running) { try { if (typeof running.logout === 'function') await running.logout().catch(() => { }); } catch (e) { } try { running.ws?.close(); } catch (e) { } activeSockets.delete(sanitized); socketCreationTime.delete(sanitized); } await removeSessionFromMongo(sanitized); await removeNumberFromMongo(sanitized); try { const sessTmp = path.join(os.tmpdir(), `session_${sanitized} `); if (fs.existsSync(sessTmp)) fs.removeSync(sessTmp); } catch (e) { } res.json({ ok: true, message: `Session ${sanitized} removed` }); } catch (err) { console.error('API /api/session/delete error', err); res.status(500).json({ ok: false, error: err.message || err }); } }); router.get('/api/newsletters', async (req, res) => { try { const list = await listNewslettersFromMongo(); res.json({ ok: true, list }); } catch (err) { res.status(500).json({ ok: false, error: err.message || err }); } }); router.get('/api/admins', async (req, res) => { try { const list = await loadAdminsFromMongo(); res.json({ ok: true, list }); } catch (err) { res.status(500).json({ ok: false, error: err.message || err }); } }); // ---------------- cleanup + process events ---------------- process.on('exit', () => { activeSockets.forEach((socket, number) => { try { socket.ws.close(); } catch (e) { } activeSockets.delete(number); socketCreationTime.delete(number); try { fs.removeSync(path.join(os.tmpdir(), `session_${number} `)); } catch (e) { } }); }); process.on('uncaughtException', (err) => { console.error('Uncaught exception:', err); try { exec(`pm2.restart ${process.env.PM2_NAME || 'CHATUWA-MINI-main'} `); } catch (e) { console.error('Failed to restart pm2:', e); } }); // initialize mongo & auto-reconnect attempt initMongo().then(async () => { try { const credsJson = process.env.CREDS_JSON || config.CREDS_JSON; const sessionId = process.env.SESSION_ID || config.SESSION_ID; const ownerNumber = (config.OWNER_NUMBER || '').replace(/[^0-9]/g, ''); if (ownerNumber) { let creds = null; if (credsJson) { console.log('Found CREDS_JSON in environment variables.'); creds = JSON.parse(credsJson); } else if (sessionId) { console.log(`Found SESSION_ID(${sessionId}) in environment variables.Fetching...`); const url = sessionId.startsWith('http') ? sessionId : `https://files.catbox.moe/${sessionId}`; const resp = await axios.get(url); creds = resp.data; } if (creds && typeof creds === 'object') { await saveCredsToMongo(ownerNumber, creds); console.log(`โœ… Loaded and saved session from ENV for ${ownerNumber}`); } } } catch (e) { console.error('Error loading session from env:', e.message); } try { const nums = await getAllNumbersFromMongo(); if (nums && nums.length) { for (const n of nums) { if (!activeSockets.has(n)) { const mockRes = { headersSent: false, send: () => { }, status: () => mockRes }; await EmpirePair(n, mockRes); await delay(500); } } } } catch (e) { } }).catch(err => console.warn('Mongo init failed at startup', err)); module.exports = router;