pair / backend /server.js
Ikyy's picture
Create backend/server.js
8ee3250 verified
// 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`);
});