Trigger82 commited on
Commit
6464677
Β·
verified Β·
1 Parent(s): d1e87e3

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +641 -175
server.js CHANGED
@@ -1,242 +1,708 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
-
4
- // Configure directories
5
- const USERS_DIR = path.join(__dirname, 'users');
6
-
7
- // Ensure directories exist with proper permissions
8
- try {
9
- fs.ensureDirSync(USERS_DIR);
10
- console.log('βœ… Users directory ready');
11
- } catch (err) {
12
- console.error('❌ Failed to setup users directory:', err);
13
- process.exit(1);
14
- }
15
 
16
- // Rest of your server code remains the same...
17
  const express = require('express');
18
- const { spawn, exec } = require('child_process');
19
- const fs = require('fs-extra');
 
20
  const path = require('path');
21
  const app = express();
22
  const http = require('http').createServer(app);
23
  const io = require('socket.io')(http);
24
- const crypto = require('crypto');
25
  const fetch = require('node-fetch');
 
26
 
27
- // Configuration
28
- const PORT = process.env.PORT || 7860;
29
- const USERS_DIR = path.join(__dirname, 'users');
30
- const SESSIONS_FILE = path.join(__dirname, 'sessions.json');
31
- const BANNED_FILE = path.join(__dirname, 'banned.json');
32
- const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || crypto.randomBytes(16).toString('hex');
33
-
34
- // Ensure directories exist
35
- fs.ensureDirSync(USERS_DIR);
36
- if (!fs.existsSync(SESSIONS_FILE)) fs.writeFileSync(SESSIONS_FILE, '{}');
37
- if (!fs.existsSync(BANNED_FILE)) fs.writeFileSync(BANNED_FILE, '[]');
38
 
39
- // Store active processes
40
- const activeProcesses = {};
 
 
 
 
 
 
41
 
42
- // Middleware
43
- app.use(express.static('public'));
44
- app.use(express.json());
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
- // Load banned users
47
- const loadBannedUsers = () => JSON.parse(fs.readFileSync(BANNED_FILE));
48
- const isBanned = (ip) => loadBannedUsers().includes(ip);
 
 
 
 
 
 
 
 
49
 
50
- // Socket.io connection handler
51
- io.on('connection', (socket) => {
52
- const clientIp = socket.handshake.address;
53
- console.log(`New connection from ${clientIp}`);
54
-
55
- // Check if banned
56
- if (isBanned(clientIp)) {
57
- socket.emit('output', '❌ You are banned from this service');
58
- socket.disconnect(true);
59
- return;
60
  }
 
61
 
62
- // Generate session ID
63
- const sessionId = crypto.randomBytes(8).toString('hex');
64
- const sessionDir = path.join(USERS_DIR, sessionId);
65
-
66
- // Ensure session directory exists
67
- fs.ensureDirSync(sessionDir);
68
-
69
- // Store session info
70
- const sessions = JSON.parse(fs.readFileSync(SESSIONS_FILE));
71
- sessions[sessionId] = {
72
- ip: clientIp,
73
- createdAt: new Date().toISOString(),
74
- lastActivity: new Date().toISOString()
75
- };
76
- fs.writeFileSync(SESSIONS_FILE, JSON.stringify(sessions, null, 2));
 
 
77
 
78
- // Handle repository cloning
79
- socket.on('clone', (repoUrl) => {
80
- if (!repoUrl.startsWith('https://github.com/')) {
81
- return socket.emit('output', '❌ Invalid GitHub repository URL');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
- socket.emit('output', `πŸ”„ Cloning ${repoUrl}...`);
85
-
86
- // Clear directory first
87
- fs.emptyDirSync(sessionDir);
 
 
 
 
 
88
 
89
- const gitClone = spawn('git', ['clone', repoUrl, '.'], { cwd: sessionDir });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
91
- gitClone.stdout.on('data', (data) => socket.emit('output', data.toString()));
92
- gitClone.stderr.on('data', (data) => socket.emit('output', data.toString()));
 
 
93
 
94
- gitClone.on('close', (code) => {
95
- if (code === 0) {
96
- socket.emit('output', 'βœ… Repository cloned successfully!');
97
- socket.emit('clone-complete');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  } else {
99
- socket.emit('output', '❌ Failed to clone repository');
 
 
 
100
  }
101
- });
 
 
 
 
 
 
102
  });
103
 
104
- // Handle dependency installation
105
- socket.on('install', (packageManager) => {
106
- if (!['npm', 'yarn'].includes(packageManager)) {
107
- return socket.emit('output', '❌ Invalid package manager');
 
 
 
 
 
 
 
 
 
 
 
108
  }
 
109
 
110
- socket.emit('output', `πŸ“¦ Installing dependencies with ${packageManager}...`);
111
-
112
- const installProcess = spawn(packageManager, ['install'], { cwd: sessionDir });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
- installProcess.stdout.on('data', (data) => socket.emit('output', data.toString()));
115
- installProcess.stderr.on('data', (data) => socket.emit('output', data.toString()));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
- installProcess.on('close', (code) => {
118
- if (code === 0) {
119
- socket.emit('output', 'βœ… Dependencies installed successfully!');
 
 
 
 
 
 
 
120
  } else {
121
- socket.emit('output', '❌ Failed to install dependencies');
122
  }
123
- });
 
 
 
124
  });
125
 
126
- // Handle application start
127
- socket.on('start', (command) => {
128
- if (!command) return socket.emit('output', '❌ No command provided');
129
-
130
- // Kill any existing process for this session
131
- if (activeProcesses[sessionId]) {
132
- activeProcesses[sessionId].kill();
133
- delete activeProcesses[sessionId];
 
 
 
 
 
 
 
 
134
  }
 
135
 
136
- socket.emit('output', `πŸš€ Starting: ${command}`);
137
-
138
- const [cmd, ...args] = command.split(' ');
139
- const process = spawn(cmd, args, { cwd: sessionDir });
 
 
 
 
 
140
 
141
- activeProcesses[sessionId] = process;
 
 
 
142
 
143
- process.stdout.on('data', (data) => socket.emit('output', data.toString()));
144
- process.stderr.on('data', (data) => socket.emit('output', data.toString()));
 
 
145
 
146
- process.on('close', (code) => {
147
- socket.emit('output', `πŸ”΄ Process exited with code ${code}`);
148
- delete activeProcesses[sessionId];
149
- });
 
 
150
  });
151
 
152
- // Handle PM2 commands
153
- socket.on('pm2', (subcommand) => {
154
- if (!['start', 'stop', 'restart', 'list', 'logs'].includes(subcommand)) {
155
- return socket.emit('output', '❌ Invalid PM2 command');
156
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
- socket.emit('output', `⚑ Executing: pm2 ${subcommand}`);
159
-
160
- const pm2Process = spawn('pm2', [subcommand], { cwd: sessionDir });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
- pm2Process.stdout.on('data', (data) => socket.emit('output', data.toString()));
163
- pm2Process.stderr.on('data', (data) => socket.emit('output', data.toString()));
 
 
 
 
 
 
 
 
 
 
 
164
 
165
- pm2Process.on('close', (code) => {
166
- socket.emit('output', `PM2 ${subcommand} completed with code ${code}`);
167
- });
 
 
 
168
  });
169
 
170
- // Handle file operations
171
- socket.on('read-file', (filePath, callback) => {
172
- const fullPath = path.join(sessionDir, filePath);
173
-
174
- fs.readFile(fullPath, 'utf8', (err, data) => {
175
- if (err) return callback({ error: err.message });
176
- callback({ content: data });
177
- });
178
  });
179
 
180
- socket.on('write-file', ({ filePath, content }, callback) => {
181
- const fullPath = path.join(sessionDir, filePath);
182
-
183
- fs.writeFile(fullPath, content, 'utf8', (err) => {
184
- if (err) return callback({ error: err.message });
185
- callback({ success: true });
186
- });
 
187
  });
188
 
189
- // Handle directory listing
190
- socket.on('list-files', (dirPath, callback) => {
191
- const fullPath = path.join(sessionDir, dirPath || '');
192
-
193
- fs.readdir(fullPath, (err, files) => {
194
- if (err) return callback({ error: err.message });
195
- callback({ files });
196
- });
197
  });
198
 
199
- // Handle admin commands
200
- socket.on('admin', ({ password, command }) => {
201
- if (password !== ADMIN_PASSWORD) {
202
- return socket.emit('output', '❌ Invalid admin password');
 
 
 
203
  }
 
204
 
205
- if (command === 'ban') {
206
- const banned = loadBannedUsers();
207
- if (!banned.includes(clientIp)) {
208
- banned.push(clientIp);
209
- fs.writeFileSync(BANNED_FILE, JSON.stringify(banned));
210
- socket.emit('output', `βœ… Banned IP: ${clientIp}`);
 
 
 
 
 
211
  } else {
212
- socket.emit('output', '⚠️ IP already banned');
213
  }
 
 
 
214
  }
215
- // Add more admin commands as needed
216
  });
217
 
218
- // Handle disconnection
219
- socket.on('disconnect', () => {
220
- console.log(`Client disconnected: ${clientIp}`);
221
- // Clean up any active processes
222
- if (activeProcesses[sessionId]) {
223
- activeProcesses[sessionId].kill();
224
- delete activeProcesses[sessionId];
 
 
 
 
 
 
 
 
 
 
225
  }
226
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  });
228
 
229
- // Start the server
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  http.listen(PORT, () => {
231
- console.log(`πŸš€ Server running on port ${PORT}`);
232
- console.log(`πŸ”‘ Admin password: ${ADMIN_PASSWORD}`);
233
  });
234
 
235
- // Error handling
236
  process.on('uncaughtException', (err) => {
237
- console.error('Uncaught Exception:', err);
238
  });
239
-
240
- process.on('unhandledRejection', (reason, promise) => {
241
- console.error('Unhandled Rejection at:', promise, 'reason:', reason);
242
  });
 
1
+ /**
2
+ * server.js
3
+ *
4
+ * A multi-user β€œpanel” for deploying and hosting WhatsApp bots (Baileys-based).
5
+ * - Stores all data in /app/storage (writable on HF Spaces).
6
+ * - Spawns bots in child processes so bot crashes don’t kill the panel.
7
+ * - Global error handlers prevent uncaught exceptions from exiting.
8
+ * - Serves a frontend in /public and accepts a β€œping” to keep the Socket.IO connection alive.
9
+ */
 
 
 
 
 
10
 
 
11
  const express = require('express');
12
+ const { spawn } = require('child_process');
13
+ const fs = require('fs');
14
+ const fsPromises = require('fs').promises;
15
  const path = require('path');
16
  const app = express();
17
  const http = require('http').createServer(app);
18
  const io = require('socket.io')(http);
 
19
  const fetch = require('node-fetch');
20
+ const crypto = require('crypto');
21
 
22
+ // ─── 1) PATHS & STORAGE SETUP ─────────────────────────────────────────────────
23
+ const DATA_DIR = path.join(__dirname, 'storage');
24
+ const USERS_DIR = path.join(DATA_DIR, 'users');
 
 
 
 
 
 
 
 
25
 
26
+ // Ensure storage directories exist
27
+ try {
28
+ fs.mkdirSync(DATA_DIR, { recursive: true });
29
+ fs.mkdirSync(USERS_DIR, { recursive: true });
30
+ } catch (err) {
31
+ console.error('Error ensuring storage directories:', err);
32
+ process.exit(1);
33
+ }
34
 
35
+ // Paths to JSON files in storage
36
+ const blueBuyerCodeFile = path.join(DATA_DIR, 'blueBuyerCode.json');
37
+ const bannedFilePath = path.join(DATA_DIR, 'banned.json');
38
+ const usersFilePath = path.join(DATA_DIR, 'users.json');
39
+
40
+ // ─── 2) INITIALIZE blueBuyerCode.json ─────────────────────────────────────────
41
+ let blueBuyerCode;
42
+ if (fs.existsSync(blueBuyerCodeFile)) {
43
+ const data = fs.readFileSync(blueBuyerCodeFile, 'utf8');
44
+ blueBuyerCode = JSON.parse(data).code;
45
+ } else {
46
+ blueBuyerCode = Math.floor(118738411 + Math.random() * 599938);
47
+ fs.writeFileSync(blueBuyerCodeFile, JSON.stringify({ code: blueBuyerCode }, null, 2));
48
+ }
49
+ console.log(`YOUR BUYER CODE: ${blueBuyerCode}`);
50
 
51
+ // ─── 3) INITIALIZE banned.json & users.json ──────────────────────────────────
52
+ if (!fs.existsSync(bannedFilePath)) {
53
+ fs.writeFileSync(bannedFilePath, JSON.stringify([], null, 2));
54
+ }
55
+ if (!fs.existsSync(usersFilePath)) {
56
+ // Create a default admin β€œBLUEX”
57
+ const initialUsers = {
58
+ BLUEX: { id: 'creator001', password: 'Taloalob,1', isAdmin: true }
59
+ };
60
+ fs.writeFileSync(usersFilePath, JSON.stringify(initialUsers, null, 2));
61
+ }
62
 
63
+ // ─── 4) JSON LOAD / SAVE HELPERS ───────────────────────────────────────────────
64
+ const loadBannedUsers = () => {
65
+ try {
66
+ const data = fs.readFileSync(bannedFilePath, 'utf8');
67
+ return JSON.parse(data);
68
+ } catch (err) {
69
+ console.error('Error loading banned users:', err);
70
+ return [];
 
 
71
  }
72
+ };
73
 
74
+ const saveBannedUsers = (bannedUsers) => {
75
+ try {
76
+ fs.writeFileSync(bannedFilePath, JSON.stringify(bannedUsers, null, 2));
77
+ } catch (err) {
78
+ console.error('Error saving banned users:', err);
79
+ }
80
+ };
81
+
82
+ const loadUsers = () => {
83
+ try {
84
+ const data = fs.readFileSync(usersFilePath, 'utf8');
85
+ return JSON.parse(data);
86
+ } catch (err) {
87
+ console.error('Error loading users:', err);
88
+ return {};
89
+ }
90
+ };
91
 
92
+ const saveUsers = (users) => {
93
+ try {
94
+ fs.writeFileSync(usersFilePath, JSON.stringify(users, null, 2));
95
+ } catch (err) {
96
+ console.error('Error saving users:', err);
97
+ }
98
+ };
99
+
100
+ // ─── 5) COUNT HELPERS ───────────────────────────────────────────────────────────
101
+ const getClientAccountCount = (clientId) => {
102
+ const users = loadUsers();
103
+ return Object.values(users).filter(u => u.clientId === clientId).length;
104
+ };
105
+
106
+ const getTotalUserCount = () => Object.keys(loadUsers()).length;
107
+ const getBannedUserCount = () => loadBannedUsers().length;
108
+
109
+ // ─── 6) CALCULATE STORAGE USAGE ────────────────────────────────────────────────
110
+ const calculateDirectorySize = (directory) => {
111
+ let totalSize = 0;
112
+ const items = fs.readdirSync(directory, { withFileTypes: true });
113
+ for (const item of items) {
114
+ const p = path.join(directory, item.name);
115
+ const stats = fs.statSync(p);
116
+ if (stats.isFile()) {
117
+ totalSize += stats.size;
118
+ } else if (stats.isDirectory()) {
119
+ totalSize += calculateDirectorySize(p);
120
  }
121
+ }
122
+ return totalSize;
123
+ };
124
+
125
+ // ─── 7) FILE OPERATIONS FOR USERS ───────────────────────────────────────────────
126
+ async function uploadFile(userId, filePath, content) {
127
+ const fullPath = path.join(USERS_DIR, userId, filePath);
128
+ try {
129
+ await fsPromises.mkdir(path.dirname(fullPath), { recursive: true });
130
+ await fsPromises.writeFile(fullPath, content, 'utf-8');
131
+ } catch (err) {
132
+ console.error('Error uploading file:', err);
133
+ throw new Error('Failed to upload file');
134
+ }
135
+ }
136
 
137
+ async function readFile(userId, filePath) {
138
+ const fullPath = path.join(USERS_DIR, userId, filePath);
139
+ try {
140
+ return await fsPromises.readFile(fullPath, 'utf-8');
141
+ } catch (err) {
142
+ console.error('Error reading file:', err);
143
+ throw new Error('Failed to read file');
144
+ }
145
+ }
146
 
147
+ async function writeFile(userId, filePath, content) {
148
+ const fullPath = path.join(USERS_DIR, userId, filePath);
149
+ try {
150
+ await fsPromises.writeFile(fullPath, content, 'utf-8');
151
+ } catch (err) {
152
+ console.error('Error writing file:', err);
153
+ throw new Error('Failed to write file');
154
+ }
155
+ }
156
+
157
+ async function listFiles(userId, dirPath = '') {
158
+ const fullPath = path.join(USERS_DIR, userId, dirPath);
159
+ try {
160
+ const items = await fsPromises.readdir(fullPath, { withFileTypes: true });
161
+ return items.map(item => ({
162
+ name: item.name,
163
+ type: item.isDirectory() ? 'folder' : 'file',
164
+ path: path.join(dirPath, item.name)
165
+ }));
166
+ } catch (err) {
167
+ console.error('Error listing files:', err);
168
+ throw new Error('Failed to list files');
169
+ }
170
+ }
171
+
172
+ // ─── 8) SERVER SETUP ───────────────────────────────────────────────────────────
173
+ app.use(express.json());
174
+ app.use(express.urlencoded({ extended: true }));
175
+ app.use(express.static(path.join(__dirname, 'public')));
176
 
177
+ // Health check endpoint for HF Spaces
178
+ app.get('/', (req, res) => {
179
+ res.send('Panel is running.');
180
+ });
181
 
182
+ // In-memory user state tracking
183
+ const userStates = {}; // { [userId]: { step: string, runningProcess: ChildProcess|null } }
184
+
185
+ // ─── 9) LOGIN / AUTH ───────────────────────────────────────────────────────────
186
+ io.on('connection', (socket) => {
187
+ console.log('A user connected');
188
+
189
+ // ─── Login ─────────────────────────────────────────────────────────────────
190
+ socket.on('login', async ({ username, password }) => {
191
+ try {
192
+ if (!username || !password) throw new Error('Missing fields');
193
+ if (typeof username !== 'string' || typeof password !== 'string') throw new Error('Invalid types');
194
+
195
+ const users = loadUsers();
196
+ const user = users[username];
197
+ if (user && user.password === password) {
198
+ socket.emit('loginResponse', {
199
+ success: true,
200
+ userId: user.id,
201
+ isAdmin: user.isAdmin
202
+ });
203
  } else {
204
+ socket.emit('loginResponse', {
205
+ success: false,
206
+ message: 'Invalid username or password.'
207
+ });
208
  }
209
+ } catch (err) {
210
+ console.error('Error during login:', err);
211
+ socket.emit('loginResponse', {
212
+ success: false,
213
+ message: 'An error occurred during login.'
214
+ });
215
+ }
216
  });
217
 
218
+ // ─── Admin: Get Users ─────────────────────────────────────────────────────
219
+ socket.on('adminGetUsers', () => {
220
+ try {
221
+ const users = loadUsers();
222
+ const userList = Object.keys(users).map(username => ({
223
+ username,
224
+ id: users[username].id,
225
+ isAdmin: users[username].isAdmin,
226
+ password: users[username].password
227
+ }));
228
+ const totalUsers = getTotalUserCount();
229
+ socket.emit('adminUserList', { users: userList, totalUsers });
230
+ } catch (err) {
231
+ console.error('Error getting user list:', err);
232
+ socket.emit('adminUserList', { users: [], totalUsers: 0 });
233
  }
234
+ });
235
 
236
+ // ─── Admin: Ban User ───────────────────────────────────────────────────────
237
+ socket.on('adminBanUser', (userId) => {
238
+ try {
239
+ if (typeof userId !== 'string') throw new Error('Invalid userId');
240
+ const bannedUsers = loadBannedUsers();
241
+ if (!bannedUsers.includes(userId)) {
242
+ bannedUsers.push(userId);
243
+ saveBannedUsers(bannedUsers);
244
+ socket.emit('adminBanResponse', { success: true, message: 'User banned.' });
245
+ io.emit('userStats', getUserStats());
246
+ } else {
247
+ socket.emit('adminBanResponse', { success: false, message: 'User already banned.' });
248
+ }
249
+ } catch (err) {
250
+ console.error('Error banning user:', err);
251
+ socket.emit('adminBanResponse', { success: false, message: 'Error banning user.' });
252
+ }
253
+ });
254
 
255
+ // ─── Admin: Unban User ─────────────────────────────────────────────────────
256
+ socket.on('adminUnbanUser', (userId) => {
257
+ try {
258
+ if (typeof userId !== 'string') throw new Error('Invalid userId');
259
+ const bannedUsers = loadBannedUsers();
260
+ const idx = bannedUsers.indexOf(userId);
261
+ if (idx > -1) {
262
+ bannedUsers.splice(idx, 1);
263
+ saveBannedUsers(bannedUsers);
264
+ socket.emit('adminUnbanResponse', { success: true, message: 'User unbanned.' });
265
+ io.emit('userStats', getUserStats());
266
+ } else {
267
+ socket.emit('adminUnbanResponse', { success: false, message: 'User not banned.' });
268
+ }
269
+ } catch (err) {
270
+ console.error('Error unbanning user:', err);
271
+ socket.emit('adminUnbanResponse', { success: false, message: 'Error unbanning user.' });
272
+ }
273
+ });
274
+
275
+ // ─── Admin: Delete User ────────────────────────────────────────────────────
276
+ socket.on('adminDeleteUser', (userId) => {
277
+ try {
278
+ if (typeof userId !== 'string') throw new Error('Invalid userId');
279
+ const users = loadUsers();
280
+ const usernameToDelete = Object.keys(users).find(u => users[u].id === userId);
281
+ if (usernameToDelete) {
282
+ delete users[usernameToDelete];
283
+ saveUsers(users);
284
+
285
+ // Remove user's folder
286
+ const userDir = path.join(USERS_DIR, userId);
287
+ if (fs.existsSync(userDir)) {
288
+ fs.rmSync(userDir, { recursive: true, force: true });
289
+ }
290
+
291
+ // Also unban if present
292
+ const bannedUsers = loadBannedUsers();
293
+ const idx = bannedUsers.indexOf(userId);
294
+ if (idx > -1) {
295
+ bannedUsers.splice(idx, 1);
296
+ saveBannedUsers(bannedUsers);
297
+ }
298
+
299
+ socket.emit('adminDeleteUserResponse', { success: true, message: 'User deleted.' });
300
+ io.emit('userStats', getUserStats());
301
+ } else {
302
+ socket.emit('adminDeleteUserResponse', { success: false, message: 'User not found.' });
303
+ }
304
+ } catch (err) {
305
+ console.error('Error deleting user:', err);
306
+ socket.emit('adminDeleteUserResponse', { success: false, message: 'Error deleting user.' });
307
+ }
308
+ });
309
 
310
+ // ─── Admin: Make Admin ──────────────────────────────────────────────────────
311
+ socket.on('adminMakeAdmin', (userId) => {
312
+ try {
313
+ if (typeof userId !== 'string') throw new Error('Invalid userId');
314
+ const users = loadUsers();
315
+ const usernameToUpdate = Object.keys(users).find(u => users[u].id === userId);
316
+ if (usernameToUpdate) {
317
+ users[usernameToUpdate].isAdmin = true;
318
+ saveUsers(users);
319
+ socket.emit('adminMakeAdminResponse', { success: true, message: 'User is now admin.' });
320
  } else {
321
+ socket.emit('adminMakeAdminResponse', { success: false, message: 'User not found.' });
322
  }
323
+ } catch (err) {
324
+ console.error('Error making user admin:', err);
325
+ socket.emit('adminMakeAdminResponse', { success: false, message: 'Error making user admin.' });
326
+ }
327
  });
328
 
329
+ // ─── Admin: Remove Admin ────────────────────────────────────────────────────
330
+ socket.on('adminRemoveAdmin', (userId) => {
331
+ try {
332
+ if (typeof userId !== 'string') throw new Error('Invalid userId');
333
+ const users = loadUsers();
334
+ const usernameToUpdate = Object.keys(users).find(u => users[u].id === userId);
335
+ if (usernameToUpdate) {
336
+ users[usernameToUpdate].isAdmin = false;
337
+ saveUsers(users);
338
+ socket.emit('adminRemoveAdminResponse', { success: true, message: 'Admin removed.' });
339
+ } else {
340
+ socket.emit('adminRemoveAdminResponse', { success: false, message: 'User not found.' });
341
+ }
342
+ } catch (err) {
343
+ console.error('Error removing admin:', err);
344
+ socket.emit('adminRemoveAdminResponse', { success: false, message: 'Error removing admin.' });
345
  }
346
+ });
347
 
348
+ // ─── User β€œStart” (makes user folder and prompts for repo) ──────────────────
349
+ socket.on('start', (userId) => {
350
+ try {
351
+ if (typeof userId !== 'string') throw new Error('Invalid userId');
352
+ const bannedUsers = loadBannedUsers();
353
+ if (bannedUsers.includes(userId)) {
354
+ socket.emit('message', '❌ You are banned from using this service.');
355
+ return;
356
+ }
357
 
358
+ const userDir = path.join(USERS_DIR, userId);
359
+ if (!fs.existsSync(userDir)) {
360
+ fs.mkdirSync(userDir, { recursive: true });
361
+ }
362
 
363
+ // Show storage usage
364
+ const spaceUsed = calculateDirectorySize(userDir);
365
+ const spaceMB = (spaceUsed / (1024 * 1024)).toFixed(2);
366
+ socket.emit('message', JSON.stringify({ type: 'spaceUsage', usage: `${spaceMB} MB` }));
367
 
368
+ userStates[userId] = { step: 'ask_repo', runningProcess: null };
369
+ socket.emit('message', 'πŸ”„ Please send the GitHub repo URL to clone (e.g. https://github.com/user/repo).');
370
+ } catch (err) {
371
+ console.error('Error starting session:', err);
372
+ socket.emit('message', '❌ Error starting your session.');
373
+ }
374
  });
375
 
376
+ // ─── User β€œCommand” Handler (clone, install, run, clear, list) ─────────────
377
+ socket.on('command', async (data) => {
378
+ try {
379
+ if (!data || typeof data.userId !== 'string' || typeof data.message !== 'string') {
380
+ throw new Error('Invalid input types');
381
+ }
382
+ const { userId, message } = data;
383
+ const bannedUsers = loadBannedUsers();
384
+ if (bannedUsers.includes(userId)) {
385
+ socket.emit('message', '❌ You are banned from using this service.');
386
+ return;
387
+ }
388
+
389
+ if (!userStates[userId] || !userStates[userId].step) {
390
+ socket.emit('message', '❌ Please click β€œStart” before sending commands.');
391
+ return;
392
+ }
393
+
394
+ const userDir = path.join(USERS_DIR, userId);
395
+ if (!fs.existsSync(userDir)) {
396
+ fs.mkdirSync(userDir, { recursive: true });
397
+ }
398
+
399
+ const state = userStates[userId];
400
+
401
+ // β€œclear” β†’ delete user's folder
402
+ if (message.toLowerCase() === 'clear') {
403
+ if (fs.existsSync(userDir)) {
404
+ socket.emit('message', 'πŸ—‘ Clearing your directory...');
405
+ const rmProc = spawn('rm', ['-rf', userDir]);
406
+
407
+ rmProc.on('error', (err) => {
408
+ console.error('❌ Failed to spawn rm:', err);
409
+ socket.emit('message', '❌ Failed to clear directory: ' + err.message);
410
+ });
411
+
412
+ rmProc.on('close', (code) => {
413
+ if (code === 0) {
414
+ socket.emit('message', 'βœ… Directory cleared.');
415
+ } else {
416
+ socket.emit('message', '❌ Failed to clear directory.');
417
+ }
418
+ });
419
+ } else {
420
+ socket.emit('message', '❌ Directory not found.');
421
+ }
422
+ return;
423
+ }
424
+
425
+ // β€œlist” β†’ list files in user's folder
426
+ if (message.toLowerCase() === 'list') {
427
+ try {
428
+ const files = await listFiles(userId);
429
+ const names = files.map(f => f.name).join('\n');
430
+ socket.emit('message', `πŸ“‚ Files:\n${names || '(empty)'}`);
431
+ } catch {
432
+ socket.emit('message', '❌ Could not list files.');
433
+ }
434
+ return;
435
+ }
436
+
437
+ // If we are in β€œask_repo” state, treat message as GitHub URL
438
+ if (state.step === 'ask_repo') {
439
+ const repoUrl = message.trim();
440
+ if (!repoUrl.startsWith('https://github.com/')) {
441
+ socket.emit('message', '❌ Invalid GitHub URL. It must start with https://github.com/');
442
+ return;
443
+ }
444
+
445
+ socket.emit('message', `πŸ”„ Cloning repo: ${repoUrl}`);
446
+ const gitClone = spawn('git', ['clone', repoUrl, '.'], { cwd: userDir });
447
+
448
+ gitClone.on('error', (err) => {
449
+ console.error('❌ Failed to spawn git clone:', err);
450
+ socket.emit('message', '❌ Failed to run git clone: ' + err.message);
451
+ });
452
+
453
+ gitClone.stdout.on('data', (data) => {
454
+ socket.emit('message', `βœ… GIT: ${data.toString()}`);
455
+ });
456
+ gitClone.stderr.on('data', (data) => {
457
+ socket.emit('message', `⚠️ GIT ERROR: ${data.toString()}`);
458
+ });
459
+
460
+ gitClone.on('close', (code) => {
461
+ if (code === 0) {
462
+ socket.emit('message', 'βœ… Repo cloned successfully! Installing dependencies...');
463
+ const yarnInstall = spawn('yarn', ['install'], { cwd: userDir });
464
+
465
+ yarnInstall.on('error', (err) => {
466
+ console.error('❌ Failed to spawn yarn install:', err);
467
+ socket.emit('message', '❌ Failed to run yarn install: ' + err.message);
468
+ });
469
+
470
+ yarnInstall.stdout.on('data', (data) => {
471
+ socket.emit('message', `βœ… YARN: ${data.toString()}`);
472
+ });
473
+ yarnInstall.stderr.on('data', (data) => {
474
+ socket.emit('message', `⚠️ YARN ERROR: ${data.toString()}`);
475
+ });
476
+ yarnInstall.on('close', (installCode) => {
477
+ if (installCode === 0) {
478
+ socket.emit('message', 'βœ… Dependencies installed.\n▢️ Send the filename to run (e.g. index.js).');
479
+ state.step = 'ask_file';
480
+ } else {
481
+ socket.emit('message', '❌ Error installing dependencies.');
482
+ }
483
+ });
484
+ } else {
485
+ socket.emit('message', '❌ Error cloning the repository.');
486
+ }
487
+ });
488
+
489
+ return;
490
+ }
491
 
492
+ // If we are in β€œask_file” state, message is the filename to run
493
+ if (state.step === 'ask_file') {
494
+ const filename = message.trim();
495
+ const filePath = path.join(userDir, filename);
496
+ if (!fs.existsSync(filePath)) {
497
+ socket.emit('message', '❌ File not found: ' + filename);
498
+ return;
499
+ }
500
+ socket.emit('message', `πŸš€ Running file: ${filename}`);
501
+
502
+ // Run the bot safely
503
+ runBot(userId, filename, socket);
504
+
505
+ // Next commands will go to β€œinteracting” state
506
+ state.step = 'interacting';
507
+ return;
508
+ }
509
 
510
+ // If we are in β€œinteracting” state, forward input to bot’s stdin
511
+ if (state.step === 'interacting') {
512
+ if (state.runningProcess) {
513
+ try {
514
+ state.runningProcess.stdin.write(message + '\n');
515
+ } catch (err) {
516
+ socket.emit('message', '❌ Failed to write to bot stdin: ' + err.message);
517
+ }
518
+ } else {
519
+ socket.emit('message', '❌ No active bot process. Please β€œStart” again.');
520
+ }
521
+ return;
522
+ }
523
 
524
+ // Otherwise, unrecognized command
525
+ socket.emit('message', '❌ Unrecognized command. Use β€œlist”, β€œclear”, or send the GitHub URL.');
526
+ } catch (err) {
527
+ console.error('Error processing command:', err);
528
+ socket.emit('message', '❌ Error processing command: ' + err.message);
529
+ }
530
  });
531
 
532
+ // ─── Read File ─────────────────────────────────────────────────────────────────
533
+ socket.on('readFile', async ({ userId, filePath }) => {
534
+ try {
535
+ const content = await readFile(userId, filePath);
536
+ socket.emit('fileContent', { filePath, content });
537
+ } catch (err) {
538
+ socket.emit('error', { message: err.message });
539
+ }
540
  });
541
 
542
+ // ─── Write File ────────────────────────────────────────────────────────────────
543
+ socket.on('writeFile', async ({ userId, filePath, content }) => {
544
+ try {
545
+ await writeFile(userId, filePath, content);
546
+ socket.emit('fileSaved', { filePath });
547
+ } catch (err) {
548
+ socket.emit('error', { message: err.message });
549
+ }
550
  });
551
 
552
+ // ─── List Files ────────────────────────────────────────────────────────────────
553
+ socket.on('listFiles', async ({ userId, dirPath }) => {
554
+ try {
555
+ const files = await listFiles(userId, dirPath);
556
+ socket.emit('fileList', files);
557
+ } catch (err) {
558
+ socket.emit('error', { message: err.message });
559
+ }
560
  });
561
 
562
+ // ─── Upload File ───────────────────────────────────────────────────────────────
563
+ socket.on('uploadFile', async ({ userId, filePath, content }) => {
564
+ try {
565
+ await uploadFile(userId, filePath, content);
566
+ socket.emit('fileUploaded', { filePath });
567
+ } catch (err) {
568
+ socket.emit('error', { message: err.message });
569
  }
570
+ });
571
 
572
+ // ─── Forgot Password: generate token ───────────────────────────────────────────
573
+ socket.on('forgotPassword', async (clientId) => {
574
+ try {
575
+ const users = loadUsers();
576
+ const userObj = Object.values(users).find(u => u.clientId === clientId);
577
+ if (userObj) {
578
+ const resetToken = crypto.randomBytes(20).toString('hex');
579
+ userObj.resetToken = resetToken;
580
+ userObj.resetTokenExpires = Date.now() + 3600000; // 1 hour
581
+ saveUsers(users);
582
+ socket.emit('resetTokenGenerated', { username: Object.keys(users).find(k => users[k].clientId === clientId), resetToken });
583
  } else {
584
+ socket.emit('resetTokenError', 'No user found with that Device ID.');
585
  }
586
+ } catch (err) {
587
+ console.error('Error in forgotPassword:', err);
588
+ socket.emit('resetTokenError', 'Error during password reset.');
589
  }
 
590
  });
591
 
592
+ // ─── Reset Password: validate token and update ─────────────────────────────────
593
+ socket.on('resetPassword', async ({ resetToken, newPassword }) => {
594
+ try {
595
+ const users = loadUsers();
596
+ const userObj = Object.values(users).find(u => u.resetToken === resetToken && u.resetTokenExpires > Date.now());
597
+ if (userObj) {
598
+ userObj.password = newPassword;
599
+ delete userObj.resetToken;
600
+ delete userObj.resetTokenExpires;
601
+ saveUsers(users);
602
+ socket.emit('passwordResetSuccess', 'Password has been reset.');
603
+ } else {
604
+ socket.emit('passwordResetError', 'Invalid or expired token.');
605
+ }
606
+ } catch (err) {
607
+ console.error('Error in resetPassword:', err);
608
+ socket.emit('passwordResetError', 'Error resetting password.');
609
  }
610
  });
611
+
612
+ // ─── Keep-Alive β€œping” (frontend sends every 10s) ──────────────────────────────
613
+ socket.on('ping', () => {
614
+ // simply acknowledges so the container doesn’t go idle
615
+ });
616
+
617
+ // ─── Get Live Stats on Demand ─────────────────────────────────────────────────
618
+ socket.on('getServerRuntime', () => {
619
+ socket.emit('serverRuntime', getServerRuntime());
620
+ });
621
+
622
+ socket.on('getSystemStatus', () => {
623
+ socket.emit('systemStatus', getSystemStatus());
624
+ });
625
+
626
+ socket.on('getUserStats', () => {
627
+ socket.emit('userStats', getUserStats());
628
+ });
629
+
630
+ // ─── Disconnect ───────────────────────────────────────────────────────────────
631
+ socket.on('disconnect', () => {
632
+ console.log('A user disconnected');
633
+ io.emit('userStats', getUserStats());
634
+ });
635
  });
636
 
637
+ // ─── 10) BROADCAST LIVE STATS EVERY 5 SECONDS ─────────────────────────────────
638
+ function getServerRuntime() {
639
+ const uptime = process.uptime();
640
+ return `${Math.floor(uptime / 3600)}h ${Math.floor((uptime % 3600) / 60)}m ${Math.floor(uptime % 60)}s`;
641
+ }
642
+
643
+ function getSystemStatus() {
644
+ // (For example purposes; expand as needed)
645
+ return {
646
+ freeMemory: (os.freemem() / (1024 * 1024)).toFixed(2) + ' MB',
647
+ totalMemory: (os.totalmem() / (1024 * 1024)).toFixed(2) + ' MB',
648
+ cpuUsage: process.cpuUsage().system + process.cpuUsage().user
649
+ };
650
+ }
651
+
652
+ function getUserStats() {
653
+ const total = getTotalUserCount();
654
+ const bannedCount = getBannedUserCount();
655
+ return { totalUsers: total, bannedUsers: bannedCount };
656
+ }
657
+
658
+ setInterval(() => {
659
+ try {
660
+ io.emit('serverRuntime', getServerRuntime());
661
+ io.emit('systemStatus', getSystemStatus());
662
+ io.emit('userStats', getUserStats());
663
+ } catch (err) {
664
+ console.error('Error emitting periodic stats:', err);
665
+ }
666
+ }, 5000);
667
+
668
+ // ─── 11) RUN BOT HELPER ────────────────────────────────────────────────────────
669
+ function runBot(userId, filename, socket) {
670
+ const userDir = path.join(USERS_DIR, userId);
671
+ const fullPath = path.join(userDir, filename);
672
+ const proc = spawn('node', [fullPath], { cwd: userDir });
673
+
674
+ // Store reference so we can send stdin later
675
+ userStates[userId].runningProcess = proc;
676
+
677
+ proc.on('error', (err) => {
678
+ console.error(`❌ Bot spawn error for ${userId}:`, err);
679
+ socket.emit('message', '❌ Failed to start bot: ' + err.message);
680
+ });
681
+
682
+ proc.stdout.on('data', (data) => {
683
+ socket.emit('message', `🟒 BOT: ${data.toString()}`);
684
+ });
685
+
686
+ proc.stderr.on('data', (data) => {
687
+ socket.emit('message', `πŸ”΄ BOT ERROR: ${data.toString()}`);
688
+ });
689
+
690
+ proc.on('close', (code) => {
691
+ socket.emit('message', `βšͺ BOT exited with code ${code}`);
692
+ userStates[userId].runningProcess = null;
693
+ });
694
+ }
695
+
696
+ // ─── 12) START THE SERVER ───────────────���─────────────────────────────────────
697
+ const PORT = process.env.PORT || 7860;
698
  http.listen(PORT, () => {
699
+ console.log(`βœ… Server running on port ${PORT}`);
 
700
  });
701
 
702
+ // ─── 13) GLOBAL ERROR HANDLERS ────────────────────────────────────────────────
703
  process.on('uncaughtException', (err) => {
704
+ console.error('πŸ’₯ Uncaught Exception:', err);
705
  });
706
+ process.on('unhandledRejection', (reason) => {
707
+ console.error('πŸ’₯ Unhandled Promise Rejection:', reason);
 
708
  });