|
|
|
|
|
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`); |
|
|
}); |