waku / server.js
arifardev's picture
Create server.js
3519f78 verified
Raw
History Blame Contribute Delete
17 kB
import express from 'express';
import makeWASocket, { useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } from '@whiskeysockets/baileys';
import QRCode from 'qrcode';
import cors from 'cors';
import pino from 'pino';
import fs from 'fs';
const app = express();
// Hugging Face menggunakan port 7860 secara default
const PORT = process.env.PORT || 7860;
app.use(cors());
app.use(express.json());
// ==========================================
// SISTEM PENGAMAN (API KEY) HUGGING FACE
// ==========================================
const API_PASSWORD = process.env.API_PASSWORD; // Diambil dari Settings -> Secrets HF
const authMiddleware = (req, res, next) => {
const userKey = req.headers['x-api-key'];
if (!API_PASSWORD) {
return res.status(500).json({ error: 'Server belum dikonfigurasi (Secret API_PASSWORD kosong)' });
}
if (userKey !== API_PASSWORD) {
return res.status(401).json({ error: 'Unauthorized: Password salah / tidak ada header x-api-key!' });
}
next();
};
// Terapkan middleware keamanan HANYA untuk semua endpoint yang berawalan /api
app.use('/api', authMiddleware);
// ==========================================
// VARIABEL GLOBAL & WEBHOOKS
// ==========================================
const sessions = new Map();
const sessionLocks = new Map(); // Kunci Mutlak Anti Race-Condition
const activeBlasts = new Map();
const logger = pino({ level: 'silent' });
const WEBHOOK_URL = 'https://jghd.space/api/webhook/wa-status';
const WEBHOOK_REPORT_URL = 'https://jghd.space/api/webhook/target-status';
const WEBHOOK_INCOMING_URL = 'https://jghd.space/api/webhook/incoming-message';
// ==========================================
// SISTEM LOCKING DENGAN LOG
// ==========================================
async function acquireLock(sessionId, actionName) {
if (sessionLocks.has(sessionId)) {
console.log(`[πŸ”’ LOCK] β›” Akses DITOLAK untuk aksi [${actionName}]. Sesi ${sessionId} sedang diproses!`);
return false;
}
console.log(`[πŸ”’ LOCK] βœ… Akses DIBERIKAN untuk aksi [${actionName}] pada sesi ${sessionId}.`);
sessionLocks.set(sessionId, true);
return true;
}
function releaseLock(sessionId) {
sessionLocks.delete(sessionId);
console.log(`[πŸ”“ UNLOCK] Kunci dilepas untuk sesi ${sessionId}.`);
}
// ==========================================
// FUNGSI PEMBUNUH DENGAN LOG
// ==========================================
async function killSession(sessionId, reason = "unknown") {
console.log(`\nπŸ’€ [KILL] Mengeksekusi killSession untuk [${sessionId}]. Alasan: ${reason}`);
if (sessions.has(sessionId)) {
const sock = sessions.get(sessionId);
try {
sock.isKilledBySystem = true;
sock.ev.removeAllListeners(); // TULIKAN SOCKET
sock.ws.close();
console.log(` -> βœ… Socket WebSocket berhasil ditutup.`);
} catch (e) {
console.log(` -> ⚠️ Socket sudah tertutup sebelumnya.`);
}
sessions.delete(sessionId);
} else {
console.log(` -> ℹ️ Sesi tidak ada di memori Map.`);
}
const sessionDir = `./sessions/${sessionId}`;
if (fs.existsSync(sessionDir)) {
fs.rmSync(sessionDir, { recursive: true, force: true });
console.log(` -> πŸ—‘οΈ Folder fisik sesi dihapus.`);
}
}
// ==========================================
// INISIALISASI KONEKSI BAILEYS V7
// ==========================================
async function initWaConnection(sessionId, reqType = 'qr', phoneNumber = null, res = null) {
console.log(`\nπŸš€ [INIT] Membangun sesi [${sessionId}] | Mode: ${reqType.toUpperCase()}`);
const sessionDir = `./sessions/${sessionId}`;
let destroyTimeout = null;
if (!fs.existsSync('./sessions')) fs.mkdirSync('./sessions');
if (!fs.existsSync(sessionDir)) fs.mkdirSync(sessionDir, { recursive: true });
const { state, saveCreds } = await useMultiFileAuthState(sessionDir);
const { version } = await fetchLatestBaileysVersion();
const sock = makeWASocket({
version,
auth: state,
logger,
browser: ['Doctor Blast', 'Chrome', '1.0.0'],
syncFullHistory: false,
markOnlineOnConnect: true,
// Di V7, otomatis menggunakan LID mapping di latar belakang
});
sessions.set(sessionId, sock);
console.log(` -> βœ… Socket berhasil disuntikkan ke memori Map.`);
sock.ev.on('creds.update', saveCreds);
if (reqType === 'pairing' && phoneNumber && !sock.authState.creds.me) {
console.log(` -> ⏳ Menunggu 3 detik untuk request kode pairing ke ${phoneNumber}...`);
setTimeout(async () => {
try {
let code = await sock.requestPairingCode(phoneNumber);
console.log(` -> πŸŽ‰ KODE PAIRING DIDAPAT: ${code}`);
if (res && !res.headersSent) res.json({ status: 'success', pairingCode: code });
if (!destroyTimeout) {
destroyTimeout = setTimeout(async () => {
console.log(`⏳ [TIMEOUT] 2 Menit Pairing habis untuk [${sessionId}]!`);
await killSession(sessionId, 'Timeout Pairing Code');
fetch(WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId, status: 'inactive' }) }).catch(() => {});
}, 120000);
}
} catch (error) {
console.log(` -> ❌ GAGAL mendapatkan Pairing Code:`, error.message);
if (res && !res.headersSent) res.status(500).json({ status: 'error', message: 'Gagal mendapatkan kode' });
}
}, 3000);
}
sock.ev.on('messages.upsert', async (m) => {
const msg = m.messages[0];
if (!msg.message || msg.key.fromMe) return;
// V7 menggunakan kombinasi LID / PN, fallback aman:
const senderNumber = msg.key.remoteJid.split('@')[0];
let incomingText = '';
let buttonId = null;
if (msg.message.interactiveResponseMessage) {
try {
const responseJson = JSON.parse(msg.message.interactiveResponseMessage.nativeFlowResponseMessage.paramsJson);
buttonId = responseJson.id;
incomingText = "[MENGKLIK TOMBOL QUICK REPLY]";
} catch (e) {}
} else {
incomingText = msg.message.conversation || msg.message.extendedTextMessage?.text || '';
}
if (incomingText || buttonId) {
console.log(`πŸ’¬ [PESAN MASUK] Sesi ${sessionId} | Dari: ${senderNumber} | Teks/ID: ${buttonId || incomingText}`);
fetch(WEBHOOK_INCOMING_URL, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, sender: senderNumber, text: incomingText, button_id: buttonId })
}).catch(() => {});
}
});
sock.ev.on('connection.update', async (update) => {
const { connection, lastDisconnect, qr } = update;
if (qr) {
console.log(` -> πŸ“Έ QR Code berhasil di-generate untuk [${sessionId}].`);
if (reqType === 'qr' && res && !res.headersSent) {
try {
const qrBase64 = await QRCode.toDataURL(qr);
res.json({ status: 'success', qr: qrBase64 });
} catch (err) {}
}
if (!destroyTimeout) {
destroyTimeout = setTimeout(async () => {
console.log(`⏳ [TIMEOUT] 2 Menit QR habis untuk [${sessionId}]!`);
await killSession(sessionId, 'Timeout QR Code');
fetch(WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId, status: 'inactive' }) }).catch(() => {});
}, 120000);
}
}
if (connection === 'close') {
if (destroyTimeout) { clearTimeout(destroyTimeout); destroyTimeout = null; }
const isKilled = sock.isKilledBySystem || false;
const statusCode = lastDisconnect?.error?.output?.statusCode;
const shouldReconnect = statusCode !== DisconnectReason.loggedOut && !isKilled;
console.log(`\n❌ [DISCONNECT] Koneksi Sesi ${sessionId} terputus.`);
console.log(` -> Status Code: ${statusCode}`);
console.log(` -> Dibunuh Sistem (isKilled): ${isKilled}`);
console.log(` -> Will Reconnect: ${shouldReconnect}`);
sessions.delete(sessionId);
if (shouldReconnect) {
console.log(` -> πŸ”„ Auto-reconnecting dalam 5 detik...`);
setTimeout(() => initWaConnection(sessionId), 5000);
} else if (!isKilled) {
console.log(` -> πŸ‘‹ PERMANENT LOGOUT via Device untuk [${sessionId}]`);
if (fs.existsSync(sessionDir)) fs.rmSync(sessionDir, { recursive: true, force: true });
fetch(WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId, status: 'inactive' }) }).catch(() => {});
}
} else if (connection === 'open') {
if (destroyTimeout) { clearTimeout(destroyTimeout); destroyTimeout = null; }
console.log(`\nβœ… [CONNECTED] Sesi ${sessionId} STABIL & SIAP DIGUNAKAN!`);
let waNumber = sock.user.id.split(':')[0];
fetch(WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sessionId, status: 'active', phone_number: waNumber }) }).catch(() => {});
}
});
return sock;
}
// ==========================================
// API ENDPOINTS
// ==========================================
app.delete('/api/session/:id', async (req, res) => {
const sessionId = req.params.id;
console.log(`\nπŸ“₯ [API] REQUEST: DELETE SESSION [${sessionId}]`);
await killSession(sessionId, 'Perintah Manual via API (Delete)');
res.json({ status: 'success' });
});
app.get('/api/session/:id/qr', (req, res) => {
console.log(`πŸ›‘οΈ [BLOCKED] Menangkis tembakan otomatis dari cache lama untuk sesi [${req.params.id}]!`);
return res.json({ status: 'blocked' });
});
app.get('/api/session/:id/request-qr', async (req, res) => {
const sessionId = req.params.id;
console.log(`\nπŸ“₯ [API] REQUEST MANUAL: GENERATE QR [${sessionId}]`);
if (sessions.has(sessionId) && sessions.get(sessionId).user) {
console.log(` -> Sesi sudah aktif, bypass QR.`);
return res.json({ status: 'connected' });
}
if (!await acquireLock(sessionId, 'Generate QR')) return res.status(429).json({ status: 'error', message: 'Sedang diproses, mohon tunggu sedetik.' });
try {
await killSession(sessionId, 'Reset Paksa sebelum Generate QR');
await initWaConnection(sessionId, 'qr', null, res);
} finally {
releaseLock(sessionId);
}
});
app.post('/api/session/:id/pairing', async (req, res) => {
const sessionId = req.params.id;
const { phoneNumber } = req.body;
console.log(`\nπŸ“₯ [API] REQUEST: PAIRING CODE [${sessionId}] untuk nomor ${phoneNumber}`);
if (!phoneNumber) return res.status(400).json({ error: 'required' });
if (sessions.has(sessionId) && sessions.get(sessionId).user) {
console.log(` -> Sesi sudah aktif, bypass Pairing.`);
return res.json({ status: 'connected' });
}
if (!await acquireLock(sessionId, 'Generate Pairing')) return res.status(429).json({ status: 'error', message: 'Sedang diproses, mohon tunggu sedetik.' });
try {
await killSession(sessionId, 'Reset Paksa sebelum Pairing Code');
await initWaConnection(sessionId, 'pairing', phoneNumber.replace(/[^0-9]/g, ''), res);
} finally {
releaseLock(sessionId);
}
});
// ==========================================
// ENDPOINT BLASTING (GRACEFUL DEGRADATION ANTI-BAN)
// ==========================================
app.post('/api/session/:id/send-bulk', async (req, res) => {
const sessionId = req.params.id;
const { targets, message, buttons, delay, is_auto_delay, user_campaign_id, image_url } = req.body;
console.log(`\nπŸš€ [API] REQUEST: SEND BULK [${sessionId}] - ${targets.length} Target`);
if (!sessions.has(sessionId)) return res.status(400).json({ status: 'error', message: 'Session tidak aktif.' });
const sock = sessions.get(sessionId);
res.json({ status: 'processing', message: `Mulai mengirim...` });
activeBlasts.set(sessionId, true);
// 1. SIAPKAN GAMBAR JIKA ADA
let imageBuffer = null;
if (image_url) {
console.log(` πŸ“Έ Mengunduh gambar dari server: ${image_url}`);
try {
const response = await fetch(image_url);
const arrayBuffer = await response.arrayBuffer();
imageBuffer = Buffer.from(arrayBuffer);
} catch (err) {
console.log(` -> ⚠️ Gagal mengunduh gambar: ${err.message}`);
}
}
// 2. SUSUN PESAN FINAL (KONVERSI TOMBOL MENJADI TEKS RAPI)
let finalMessage = message || '';
if (buttons && buttons.length > 0) {
finalMessage += '\n\n';
buttons.forEach((btn) => {
let btnType = btn.type || 'cta_url';
let btnValue = btn.value || btn.url;
if (btnType === 'cta_url') {
finalMessage += `πŸ”— *${btn.display_text}* :\n${btnValue}\n\n`;
} else if (btnType === 'cta_copy') {
finalMessage += `πŸ“‹ *${btn.display_text}* :\n${btnValue}\n\n`;
} else if (btnType === 'cta_call') {
finalMessage += `πŸ“ž *${btn.display_text}* :\nwa.me/${btnValue.replace(/[^0-9]/g, '')}\n\n`;
} else if (btnType === 'quick_reply') {
finalMessage += `πŸ’¬ *${btn.display_text}* :\nBalas dengan mengetik: *${btnValue}*\n\n`;
}
});
}
// 3. PROSES PENGIRIMAN LOOPING
for (let i = 0; i < targets.length; i++) {
if (!activeBlasts.get(sessionId)) {
console.log(`πŸ›‘ [BLAST] PROSES BLASTING SESI ${sessionId} DIHENTIKAN PAKSA!`);
break;
}
let targetJid = targets[i] + '@s.whatsapp.net';
try {
if (imageBuffer) {
await sock.sendMessage(targetJid, { image: imageBuffer, caption: finalMessage.trim() });
} else {
await sock.sendMessage(targetJid, { text: finalMessage.trim() });
}
console.log(` -> [SUKSES] Pesan terkirim ke ${targets[i]}`);
if (user_campaign_id) {
fetch(WEBHOOK_REPORT_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_campaign_id: user_campaign_id, phone_number: targets[i], status: 'sent' })
}).catch(() => {});
}
} catch (err) {
console.log(` -> [GAGAL] Pesan ke ${targets[i]} gagal dikirim:`, err.message);
}
if (i < targets.length - 1) {
let delaySecs = is_auto_delay ? Math.floor(Math.random() * (15 - 5 + 1)) + 5 : parseInt(delay);
if (delaySecs > 0) await new Promise(r => setTimeout(r, delaySecs * 1000));
}
}
activeBlasts.delete(sessionId);
console.log(`πŸŽ‰ [BLAST] Blasting sesi ${sessionId} SELESAI!`);
});
app.post('/api/session/:id/stop-bulk', (req, res) => {
const sessionId = req.params.id;
console.log(`\nπŸ›‘ [API] REQUEST: STOP BULK [${sessionId}]`);
if (activeBlasts.has(sessionId) && activeBlasts.get(sessionId) === true) {
activeBlasts.set(sessionId, false);
return res.json({ status: 'success' });
}
return res.json({ status: 'ignored' });
});
app.post('/api/session/:id/send-message', async (req, res) => {
const sessionId = req.params.id;
const { target, message } = req.body;
console.log(`\nπŸ€– [API] REQUEST: AUTO REPLY [${sessionId}] ke ${target}`);
if (!sessions.has(sessionId)) return res.status(400).json({ error: 'Session tidak aktif' });
try {
await sessions.get(sessionId).sendMessage(target + '@s.whatsapp.net', { text: message });
console.log(` -> βœ… Auto reply sukses.`);
res.json({ success: true });
} catch (err) {
console.log(` -> ❌ Auto reply gagal:`, err.message);
res.status(500).json({ error: err.message });
}
});
// Ubah binding IP menjadi '0.0.0.0' agar bisa diakses public di container Hugging Face
app.listen(PORT, '0.0.0.0', () => console.log(`πŸš€ Doctor Blast Baileys Server running on port ${PORT}`));