| 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(); |
| |
| const PORT = process.env.PORT || 7860; |
|
|
| app.use(cors()); |
| app.use(express.json()); |
|
|
| |
| |
| |
| const API_PASSWORD = process.env.API_PASSWORD; |
|
|
| 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(); |
| }; |
|
|
| |
| app.use('/api', authMiddleware); |
|
|
| |
| |
| |
| const sessions = new Map(); |
| const sessionLocks = new Map(); |
| 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'; |
|
|
| |
| |
| |
| 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}.`); |
| } |
|
|
| |
| |
| |
| 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(); |
| 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.`); |
| } |
| } |
|
|
| |
| |
| |
| 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, |
| |
| }); |
|
|
| 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; |
|
|
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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); |
| } |
| }); |
|
|
| |
| |
| |
| 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); |
|
|
| |
| 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}`); |
| } |
| } |
|
|
| |
| 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`; |
| } |
| }); |
| } |
|
|
| |
| 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 }); |
| } |
| }); |
|
|
| |
| app.listen(PORT, '0.0.0.0', () => console.log(`π Doctor Blast Baileys Server running on port ${PORT}`)); |