// backend/server.js import express from "express"; import { createServer } from "http"; import { Server } from "socket.io"; import makeWASocket, { useMultiFileAuthState, DisconnectReason, Browsers, makeCacheableSignalKeyStore } from "@whiskeysockets/baileys"; import Pino from "pino"; import { Boom } from "@hapi/boom"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import NodeCache from "node-cache"; import { v4 as uuidv4 } from "uuid"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const httpServer = createServer(app); const io = new Server(httpServer, { cors: { origin: process.env.FRONTEND_URL || "*", methods: ["GET", "POST"] } }); // Session cache - expire setelah 10 menit const sessionCache = new NodeCache({ stdTTL: 600, checkperiod: 60 }); const activeSessions = new Map(); // Middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Serve static files from frontend build const frontendPath = path.join(__dirname, "..", "frontend", "dist"); if (fs.existsSync(frontendPath)) { app.use(express.static(frontendPath)); } // Health check app.get("/health", (req, res) => { res.json({ status: "ok", activeSessions: activeSessions.size, timestamp: new Date().toISOString() }); }); // Serve frontend for all other routes app.get("*", (req, res) => { const indexPath = path.join(__dirname, "..", "frontend", "dist", "index.html"); if (fs.existsSync(indexPath)) { res.sendFile(indexPath); } else { res.status(404).json({ error: "Frontend not found. Please build the frontend first." }); } }); // Cleanup function function cleanupSession(sessionId) { const sessionDir = path.join(__dirname, "sessions", sessionId); try { if (fs.existsSync(sessionDir)) { fs.rmSync(sessionDir, { recursive: true, force: true }); console.log(`āœ… Session ${sessionId} cleaned up`); } activeSessions.delete(sessionId); sessionCache.del(sessionId); } catch (error) { console.error(`āŒ Cleanup error for ${sessionId}:`, error.message); } } // Socket.IO connection handler io.on("connection", (socket) => { console.log(`šŸ”Œ Client connected: ${socket.id}`); socket.on("start-pairing", async ({ phoneNumber }) => { // Generate unique session ID const sessionId = uuidv4(); const sessionDir = path.join(__dirname, "sessions", sessionId); try { // Validasi nomor - support semua negara const cleanNumber = phoneNumber.replace(/[^\d]/g, ''); if (!cleanNumber || cleanNumber.length < 10 || cleanNumber.length > 15) { socket.emit("error", { message: "Invalid phone number! Must be 10-15 digits" }); return; } // Pastikan dimulai dengan country code (bukan 0) if (cleanNumber.startsWith('0')) { socket.emit("error", { message: "Phone number must include country code (e.g., 1, 44, 62, 91)" }); return; } // Check session limit if (activeSessions.size >= 50) { socket.emit("error", { message: "Server is full, please try again later" }); return; } socket.emit("status", { message: "Starting connection...", sessionId }); // Create session directory if (!fs.existsSync(sessionDir)) { fs.mkdirSync(sessionDir, { recursive: true }); } const { state, saveCreds } = await useMultiFileAuthState(sessionDir); const sock = makeWASocket({ auth: { creds: state.creds, keys: makeCacheableSignalKeyStore( state.keys, Pino().child({ level: "fatal", stream: "store" }) ) }, browser: Browsers.ubuntu("Chrome"), logger: Pino({ level: "silent" }), printQRInTerminal: false, syncFullHistory: false, markOnlineOnConnect: false }); // Store socket reference activeSessions.set(sessionId, { sock, phoneNumber, socketId: socket.id }); sessionCache.set(sessionId, { phoneNumber, createdAt: Date.now() }); sock.ev.on("creds.update", saveCreds); // Connection update handler sock.ev.on("connection.update", async (update) => { const { connection, lastDisconnect } = update; if (connection === "close") { const shouldReconnect = (lastDisconnect?.error)?.output?.statusCode !== DisconnectReason.loggedOut; if (shouldReconnect) { socket.emit("error", { message: "Connection closed, please try again" }); } cleanupSession(sessionId); } else if (connection === "open") { socket.emit("status", { message: "Connected! Sending creds.json..." }); console.log(`āœ… Connected for session ${sessionId}`); // Tunggu 5 detik await new Promise(resolve => setTimeout(resolve, 5000)); try { // Baca creds.json const credsPath = path.join(sessionDir, "creds.json"); const credsContent = fs.readFileSync(credsPath, "utf8"); // Kirim file ke nomor bot await sock.sendMessage(`${cleanNumber}@s.whatsapp.net`, { document: Buffer.from(credsContent), mimetype: "application/json", fileName: "creds.json", caption: "āœ… Session created successfully!\n\nSave this file securely." }); socket.emit("success", { message: "creds.json sent successfully!", sessionId }); console.log(`šŸ“¤ Creds sent to ${cleanNumber}`); // Tunggu sebentar buat mastiin file terkirim await new Promise(resolve => setTimeout(resolve, 2000)); // Disconnect & cleanup await sock.logout(); cleanupSession(sessionId); socket.emit("status", { message: "Done! Session cleaned up." }); } catch (error) { console.error(`āŒ Send file error:`, error); socket.emit("error", { message: "Failed to send file" }); cleanupSession(sessionId); } } }); // Request pairing code setTimeout(async () => { try { if (!sock.authState.creds.registered) { const code = await sock.requestPairingCode(cleanNumber); socket.emit("pairing-code", { code, sessionId }); console.log(`šŸ“± Pairing code for ${cleanNumber}: ${code}`); } } catch (error) { console.error(`āŒ Pairing code error:`, error); socket.emit("error", { message: "Failed to get pairing code" }); cleanupSession(sessionId); } }, 3000); } catch (error) { console.error(`āŒ Connection error:`, error); socket.emit("error", { message: error.message || "An error occurred" }); cleanupSession(sessionId); } }); socket.on("disconnect", () => { console.log(`šŸ”Œ Client disconnected: ${socket.id}`); // Cleanup sessions milik socket ini for (const [sessionId, data] of activeSessions.entries()) { if (data.socketId === socket.id) { cleanupSession(sessionId); } } }); }); // Cleanup old sessions every 5 minutes setInterval(() => { const sessionsDir = path.join(__dirname, "sessions"); if (fs.existsSync(sessionsDir)) { const sessions = fs.readdirSync(sessionsDir); const now = Date.now(); const maxAge = 10 * 60 * 1000; // 10 menit sessions.forEach(sessionId => { const sessionData = sessionCache.get(sessionId); if (!sessionData || (now - sessionData.createdAt) > maxAge) { cleanupSession(sessionId); } }); } }, 5 * 60 * 1000); // Graceful shutdown process.on("SIGINT", () => { console.log("\nā¹ļø Shutting down..."); // Cleanup semua active sessions for (const sessionId of activeSessions.keys()) { cleanupSession(sessionId); } process.exit(0); }); const PORT = process.env.PORT || 7860; httpServer.listen(PORT, "0.0.0.0", () => { console.log(`šŸš€ Server running on http://0.0.0.0:${PORT}`); console.log(`šŸ”Œ Socket.IO ready`); console.log(`šŸ“± Baileys version: 6.7.20\n`); });