Trigger82 commited on
Commit
b49f2b6
Β·
verified Β·
1 Parent(s): b972e65

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +190 -478
server.js CHANGED
@@ -2,356 +2,191 @@
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
 
@@ -360,29 +195,32 @@ io.on('connection', (socket) => {
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
 
@@ -392,23 +230,16 @@ io.on('connection', (socket) => {
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.');
@@ -422,19 +253,19 @@ io.on('connection', (socket) => {
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/')) {
@@ -446,12 +277,12 @@ io.on('connection', (socket) => {
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()}`);
@@ -463,16 +294,17 @@ io.on('connection', (socket) => {
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).');
@@ -489,7 +321,7 @@ io.on('connection', (socket) => {
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);
@@ -498,20 +330,17 @@ io.on('connection', (socket) => {
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
  }
@@ -521,15 +350,15 @@ io.on('connection', (socket) => {
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);
@@ -539,7 +368,7 @@ io.on('connection', (socket) => {
539
  }
540
  });
541
 
542
- // ─── Write File ────────────────────────────────────────────────────────────────
543
  socket.on('writeFile', async ({ userId, filePath, content }) => {
544
  try {
545
  await writeFile(userId, filePath, content);
@@ -549,17 +378,7 @@ io.on('connection', (socket) => {
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);
@@ -569,137 +388,30 @@ io.on('connection', (socket) => {
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
  });
 
2
  * server.js
3
  *
4
  * A multi-user β€œpanel” for deploying and hosting WhatsApp bots (Baileys-based).
5
+ * - Stores all data in /users (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 http = require('http');
13
+ const socketIO = require('socket.io');
 
14
  const path = require('path');
15
+ const fs = require('fs');
16
+ const { spawn } = require('child_process');
17
+ const cors = require('cors');
18
+
19
+ // Create server
20
  const app = express();
21
+ const httpServer = http.createServer(app);
22
+ const io = socketIO(httpServer, {
23
+ cors: {
24
+ origin: '*',
25
+ },
26
+ });
27
 
28
  // ─── 1) PATHS & STORAGE SETUP ─────────────────────────────────────────────────
29
+ const USERS_DIR = path.join(__dirname, 'users');
30
+ if (!fs.existsSync(USERS_DIR)) fs.mkdirSync(USERS_DIR);
 
 
 
 
 
 
 
 
 
31
 
32
+ // In-memory state per user
33
+ const userStates = {}; // { [userId]: { step: string, runningProcess: ChildProcess|null } }
34
+ let activeUsers = 0;
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
+ // ─── 2) LOAD / SAVE USERS & BANNED ────────────────────────────────────────────
37
+ function loadUsers() {
38
+ const file = path.join(__dirname, 'users.json');
39
+ if (!fs.existsSync(file)) return {};
40
+ try {
41
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
42
+ } catch {
43
+ return {};
44
+ }
45
  }
46
+ function saveUsers(users) {
47
+ fs.writeFileSync(path.join(__dirname, 'users.json'), JSON.stringify(users, null, 2));
 
 
 
 
48
  }
49
 
50
+ function loadBannedUsers() {
51
+ const file = path.join(__dirname, 'banned.json');
52
+ if (!fs.existsSync(file)) return [];
53
  try {
54
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
55
+ } catch {
 
 
56
  return [];
57
  }
58
+ }
59
+ function saveBannedUsers(bannedList) {
60
+ fs.writeFileSync(path.join(__dirname, 'banned.json'), JSON.stringify(bannedList, null, 2));
61
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
+ // ─── 3) HELPER: CALCULATE STORAGE USAGE ────────────────────────────────────────
64
+ function calculateDirectorySize(dirPath) {
65
+ let totalSize = 0;
66
+ if (!fs.existsSync(dirPath)) return 0;
67
+
68
+ function walk(directory) {
69
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
70
+ for (const entry of entries) {
71
+ const fullPath = path.join(directory, entry.name);
72
+ const stats = fs.statSync(fullPath);
73
+ if (stats.isFile()) {
74
+ totalSize += stats.size;
75
+ } else if (stats.isDirectory()) {
76
+ walk(fullPath);
77
+ }
78
+ }
79
  }
 
80
 
81
+ walk(dirPath);
82
+ return totalSize;
83
+ }
 
 
84
 
85
+ // ─── 4) HELPER: RUN BOT ────────────────────────────────────────────────────────
86
+ function runBot(userId, filename, socket) {
87
+ const userDir = path.join(USERS_DIR, userId);
88
+ const fullFilePath = path.join(userDir, filename);
89
 
90
+ if (!fs.existsSync(fullFilePath)) {
91
+ socket.emit('message', '❌ Bot file not found: ' + filename);
92
+ return;
 
 
 
 
 
 
 
 
 
93
  }
 
 
94
 
 
 
 
95
  try {
96
+ const child = spawn('node', [filename], {
97
+ cwd: userDir,
98
+ stdio: ['pipe', 'pipe', 'pipe']
99
+ });
100
+
101
+ // Store reference so we can send stdin later
102
+ userStates[userId].runningProcess = child;
103
+
104
+ child.stdout.on('data', (data) => {
105
+ socket.emit('message', `πŸ“€ ${data.toString()}`);
106
+ });
107
+
108
+ child.stderr.on('data', (data) => {
109
+ socket.emit('message', `⚠️ BOT ERROR: ${data.toString()}`);
110
+ });
111
+
112
+ child.on('close', (code) => {
113
+ socket.emit('message', `πŸ›‘ Bot exited with code ${code}`);
114
+ userStates[userId].runningProcess = null;
115
+ });
116
+
117
+ child.on('error', (err) => {
118
+ socket.emit('message', '❌ Failed to spawn bot: ' + err.message);
119
+ userStates[userId].runningProcess = null;
120
+ });
121
  } catch (err) {
122
+ socket.emit('message', '❌ Exception running bot: ' + err.message);
 
123
  }
124
  }
125
 
126
+ // ─── 5) HELPER: FILE OPERATIONS ───────────────────────────────────────────────
127
+ async function listFiles(userId, dirPath = '') {
128
+ const fullDir = path.join(USERS_DIR, userId, dirPath);
129
+ if (!fs.existsSync(fullDir)) return [];
130
+ return fs.readdirSync(fullDir).map((name) => ({ name }));
131
+ }
132
+
133
  async function readFile(userId, filePath) {
134
  const fullPath = path.join(USERS_DIR, userId, filePath);
135
+ if (!fs.existsSync(fullPath)) throw new Error('File not found');
136
+ return fs.readFileSync(fullPath, 'utf8');
 
 
 
 
137
  }
138
 
139
  async function writeFile(userId, filePath, content) {
140
  const fullPath = path.join(USERS_DIR, userId, filePath);
141
+ fs.writeFileSync(fullPath, content, 'utf8');
 
 
 
 
 
142
  }
143
 
144
+ async function uploadFile(userId, filePath, contentBase64) {
145
+ const fullPath = path.join(USERS_DIR, userId, filePath);
146
+ const buffer = Buffer.from(contentBase64, 'base64');
147
+ fs.writeFileSync(fullPath, buffer);
 
 
 
 
 
 
 
 
 
148
  }
149
 
150
+ // ��── 6) HELPER: STATS ─────────────────────────────────────────────────────────
151
+ function getServerRuntime() {
152
+ const u = process.uptime();
153
+ const hours = Math.floor(u / 3600);
154
+ const minutes = Math.floor((u % 3600) / 60);
155
+ const seconds = Math.floor(u % 60);
156
+ return `${hours}h ${minutes}m ${seconds}s`;
157
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
+ function getSystemStatus() {
160
+ return {
161
+ memoryUsage: process.memoryUsage(),
162
+ uptime: process.uptime()
163
+ };
164
+ }
 
 
 
 
 
 
 
 
 
165
 
166
+ function getUserStats() {
167
+ return { activeUsers };
168
+ }
 
 
 
 
169
 
170
+ // ─── 7) STATIC & CORS ─────────────────────────────────────────────────────────
171
+ app.use(cors());
172
+ app.use(express.static(path.join(__dirname, 'public')));
 
 
 
 
 
 
 
173
 
174
+ // ─── 8) WEBSOCKET HANDLERS ────────────────────────────────────────────────────
175
+ io.on('connection', (socket) => {
176
+ activeUsers++;
177
+ io.emit('userStats', getUserStats());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
+ // ─── Start Session ─────────────────────────────────────────────────────────
180
+ socket.on('start', (userId) => {
181
  try {
182
+ if (typeof userId !== 'string' || userId.trim() === '') {
183
+ socket.emit('message', '❌ Invalid userId.');
184
+ return;
 
 
 
 
 
 
185
  }
 
 
 
 
 
186
 
187
+ const bannedList = loadBannedUsers();
188
+ if (bannedList.includes(userId)) {
189
+ socket.emit('message', '❌ You are banned from this service.');
 
 
 
 
190
  return;
191
  }
192
 
 
195
  fs.mkdirSync(userDir, { recursive: true });
196
  }
197
 
198
+ const used = calculateDirectorySize(userDir);
199
+ socket.emit('message', JSON.stringify({
200
+ type: 'spaceUsage',
201
+ usage: `${(used / 1024 / 1024).toFixed(2)} MB`
202
+ }));
203
 
204
+ // Set state to ask for GitHub repo
205
  userStates[userId] = { step: 'ask_repo', runningProcess: null };
206
+ socket.emit('message', 'πŸ”„ Please send your GitHub repo URL (e.g., https://github.com/user/repo).');
207
  } catch (err) {
208
+ console.error('Error in start:', err);
209
+ socket.emit('message', '❌ Error starting session: ' + err.message);
210
  }
211
  });
212
 
213
+ // ─── Command Handler ────────────────────────────────────────────────────────
214
+ socket.on('command', async ({ userId, message }) => {
215
  try {
216
+ if (typeof userId !== 'string' || typeof message !== 'string') {
217
+ socket.emit('message', '❌ Invalid command data.');
218
+ return;
219
  }
220
+
221
+ const bannedList = loadBannedUsers();
222
+ if (bannedList.includes(userId)) {
223
+ socket.emit('message', '❌ You are banned from this service.');
224
  return;
225
  }
226
 
 
230
  }
231
 
232
  const userDir = path.join(USERS_DIR, userId);
 
 
 
 
233
  const state = userStates[userId];
234
 
235
+ // β€”β€”β€” β€œclear” β†’ delete user’s entire directory β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
236
  if (message.toLowerCase() === 'clear') {
237
  if (fs.existsSync(userDir)) {
 
238
  const rmProc = spawn('rm', ['-rf', userDir]);
 
239
  rmProc.on('error', (err) => {
240
+ console.error('Error spawning rm:', err);
241
  socket.emit('message', '❌ Failed to clear directory: ' + err.message);
242
  });
 
243
  rmProc.on('close', (code) => {
244
  if (code === 0) {
245
  socket.emit('message', 'βœ… Directory cleared.');
 
253
  return;
254
  }
255
 
256
+ // β€”β€”β€” β€œlist” β†’ list files in user’s folder β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
257
  if (message.toLowerCase() === 'list') {
258
  try {
259
  const files = await listFiles(userId);
260
+ const names = files.map(f => f.name).join('\n') || '(empty)';
261
+ socket.emit('message', `πŸ“‚ Files:\n${names}`);
262
+ } catch (err) {
263
+ socket.emit('message', '❌ Could not list files: ' + err.message);
264
  }
265
  return;
266
  }
267
 
268
+ // β€”β€”β€” If state = β€œask_repo”, treat message as GitHub URL β€”β€”β€”β€”β€”β€”
269
  if (state.step === 'ask_repo') {
270
  const repoUrl = message.trim();
271
  if (!repoUrl.startsWith('https://github.com/')) {
 
277
  const gitClone = spawn('git', ['clone', repoUrl, '.'], { cwd: userDir });
278
 
279
  gitClone.on('error', (err) => {
280
+ console.error('Error spawning git clone:', err);
281
  socket.emit('message', '❌ Failed to run git clone: ' + err.message);
282
  });
283
 
284
  gitClone.stdout.on('data', (data) => {
285
+ socket.emit('message', `πŸ“¦ GIT: ${data.toString()}`);
286
  });
287
  gitClone.stderr.on('data', (data) => {
288
  socket.emit('message', `⚠️ GIT ERROR: ${data.toString()}`);
 
294
  const yarnInstall = spawn('yarn', ['install'], { cwd: userDir });
295
 
296
  yarnInstall.on('error', (err) => {
297
+ console.error('Error spawning yarn install:', err);
298
  socket.emit('message', '❌ Failed to run yarn install: ' + err.message);
299
  });
300
 
301
  yarnInstall.stdout.on('data', (data) => {
302
+ socket.emit('message', `πŸ“¦ YARN: ${data.toString()}`);
303
  });
304
  yarnInstall.stderr.on('data', (data) => {
305
  socket.emit('message', `⚠️ YARN ERROR: ${data.toString()}`);
306
  });
307
+
308
  yarnInstall.on('close', (installCode) => {
309
  if (installCode === 0) {
310
  socket.emit('message', 'βœ… Dependencies installed.\n▢️ Send the filename to run (e.g. index.js).');
 
321
  return;
322
  }
323
 
324
+ // β€”β€”β€” If state = β€œask_file”, message = filename to run β€”β€”β€”β€”β€”β€”β€”β€”
325
  if (state.step === 'ask_file') {
326
  const filename = message.trim();
327
  const filePath = path.join(userDir, filename);
 
330
  return;
331
  }
332
  socket.emit('message', `πŸš€ Running file: ${filename}`);
 
 
333
  runBot(userId, filename, socket);
 
 
334
  state.step = 'interacting';
335
  return;
336
  }
337
 
338
+ // β€”β€”β€” If state = β€œinteracting”, forward input to bot’s stdin β€”β€”β€”β€”
339
  if (state.step === 'interacting') {
340
+ const proc = state.runningProcess;
341
+ if (proc) {
342
  try {
343
+ proc.stdin.write(message + '\n');
344
  } catch (err) {
345
  socket.emit('message', '❌ Failed to write to bot stdin: ' + err.message);
346
  }
 
350
  return;
351
  }
352
 
353
+ // β€”β€”β€” Unrecognized command β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
354
+ socket.emit('message', '❌ Unrecognized command. Use β€œlist”, β€œclear”, or send the GitHub URL/file.');
355
  } catch (err) {
356
+ console.error('Error in command handler:', err);
357
+ socket.emit('message', '❌ Error: ' + err.message);
358
  }
359
  });
360
 
361
+ // ─── Read File ───────────────────────────────────────────────────────────────
362
  socket.on('readFile', async ({ userId, filePath }) => {
363
  try {
364
  const content = await readFile(userId, filePath);
 
368
  }
369
  });
370
 
371
+ // ─── Write File ──────────────────────────────────────────────────────────────
372
  socket.on('writeFile', async ({ userId, filePath, content }) => {
373
  try {
374
  await writeFile(userId, filePath, content);
 
378
  }
379
  });
380
 
381
+ // ─── Upload File ──────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
382
  socket.on('uploadFile', async ({ userId, filePath, content }) => {
383
  try {
384
  await uploadFile(userId, filePath, content);
 
388
  }
389
  });
390
 
391
+ // ─── Keep-Alive β€œping” (frontend sends every 10s) ─────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  socket.on('ping', () => {
393
+ // Do nothing; prevents HF Spaces from idling
 
 
 
 
 
 
 
 
 
 
 
 
 
394
  });
395
 
396
  // ─── Disconnect ───────────────────────────────────────────────────────────────
397
  socket.on('disconnect', () => {
398
+ activeUsers--;
399
  io.emit('userStats', getUserStats());
400
  });
401
  });
402
 
403
+ // ─── 9) BROADCAST LIVE STATS EVERY 5 SECONDS ─────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  setInterval(() => {
405
+ io.emit('serverRuntime', getServerRuntime());
406
+ io.emit('systemStatus', getSystemStatus());
407
+ io.emit('userStats', getUserStats());
 
 
 
 
408
  }, 5000);
409
 
410
+ // ─── 10) START THE SERVER ─────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
411
  const PORT = process.env.PORT || 7860;
412
+ httpServer.listen(PORT, () => console.log(`βœ… Server running on port ${PORT}`));
 
 
413
 
414
+ // ─── 11) GLOBAL ERROR HANDLERS ────────────────────────────────────────────────
415
  process.on('uncaughtException', (err) => {
416
  console.error('πŸ’₯ Uncaught Exception:', err);
417
  });