| |
| 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"] |
| } |
| }); |
|
|
| |
| const sessionCache = new NodeCache({ stdTTL: 600, checkperiod: 60 }); |
| const activeSessions = new Map(); |
|
|
| |
| app.use(express.json()); |
| app.use(express.urlencoded({ extended: true })); |
|
|
| |
| const frontendPath = path.join(__dirname, "frontend", "dist"); |
| if (fs.existsSync(frontendPath)) { |
| app.use(express.static(frontendPath)); |
| } |
|
|
| |
| app.get("/health", (req, res) => { |
| res.json({ |
| status: "ok", |
| activeSessions: activeSessions.size, |
| timestamp: new Date().toISOString() |
| }); |
| }); |
|
|
| |
| 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." }); |
| } |
| }); |
|
|
| |
| 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); |
| } |
| } |
|
|
| |
| io.on("connection", (socket) => { |
| console.log(`π Client connected: ${socket.id}`); |
| |
| socket.on("start-pairing", async ({ phoneNumber }) => { |
| |
| const sessionId = uuidv4(); |
| const sessionDir = path.join(__dirname, "sessions", sessionId); |
| |
| try { |
| |
| 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; |
| } |
| |
| |
| if (cleanNumber.startsWith('0')) { |
| socket.emit("error", { message: "Phone number must include country code (e.g., 1, 44, 62, 91)" }); |
| return; |
| } |
| |
| |
| if (activeSessions.size >= 50) { |
| socket.emit("error", { message: "Server is full, please try again later" }); |
| return; |
| } |
| |
| socket.emit("status", { message: "Starting connection...", sessionId }); |
| |
| |
| 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 |
| }); |
| |
| |
| activeSessions.set(sessionId, { sock, phoneNumber, socketId: socket.id }); |
| sessionCache.set(sessionId, { phoneNumber, createdAt: Date.now() }); |
| |
| sock.ev.on("creds.update", saveCreds); |
| |
| |
| 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}`); |
| |
| |
| await new Promise(resolve => setTimeout(resolve, 5000)); |
| |
| try { |
| |
| const credsPath = path.join(sessionDir, "creds.json"); |
| const credsContent = fs.readFileSync(credsPath, "utf8"); |
| |
| |
| 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}`); |
| |
| |
| await new Promise(resolve => setTimeout(resolve, 2000)); |
| |
| |
| 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); |
| } |
| } |
| }); |
| |
| |
| 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}`); |
| |
| |
| for (const [sessionId, data] of activeSessions.entries()) { |
| if (data.socketId === socket.id) { |
| cleanupSession(sessionId); |
| } |
| } |
| }); |
| }); |
|
|
| |
| 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; |
| |
| sessions.forEach(sessionId => { |
| const sessionData = sessionCache.get(sessionId); |
| |
| if (!sessionData || (now - sessionData.createdAt) > maxAge) { |
| cleanupSession(sessionId); |
| } |
| }); |
| } |
| }, 5 * 60 * 1000); |
|
|
| |
| process.on("SIGINT", () => { |
| console.log("\nβΉοΈ Shutting down..."); |
| |
| |
| 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`); |
| }); |