File size: 9,921 Bytes
8ee3250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
// 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`);
});