host / pair.js
srinetwork's picture
Upload 10 files
e77764b verified
Raw History Blame Contribute Delete
66.9 kB
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;