import express, { Request, Response, NextFunction } from 'express'; import { spawn, execSync, ChildProcess } from 'child_process'; import fs from 'fs'; import path from 'path'; interface BotProcess { child: ChildProcess; tempDir: string; startedAt: number; } interface Command { filename: string; content: string; } interface RunRequestBody { botId: string; discordId: string; envFile: string; zbrJson: string; commands: Command[]; dbBase64: string; } interface StopRequestBody { botId: string; discordId: string; } const app = express(); app.use(express.json({ limit: '10mb' })); const ZBR_SECRET = process.env.ZBR_SECRET; const processes = new Map(); try { const version = execSync('/usr/local/bin/zbr --version').toString().trim(); const size = execSync('wc -c < /usr/local/bin/zbr').toString().trim(); console.log('ZBR binary version:', version, '| size:', size, 'bytes'); } catch (e) { console.error("Failed to execute ZBR binary:", e); } const auth = (req: Request, res: Response, next: NextFunction): void | Response => { if (req.path === '/health') return next(); if (req.headers['x-zbr-secret'] !== ZBR_SECRET) { return res.status(401).send('Unauthorized'); } next(); }; app.use(auth); app.post('/run', (req: Request<{}, any, RunRequestBody>, res: Response) => { const { botId, discordId, envFile, zbrJson, commands, dbBase64 } = req.body; const key = `${discordId}-${botId}`; const userHasRunningBot = Array.from(processes.keys()).some(k => k.startsWith(`${discordId}-`)); if (userHasRunningBot) return res.status(400).send('You already have a bot running. Stop it before starting another.'); if (processes.has(key)) return res.status(400).send('Bot already running'); const tempDir = `/tmp/zbr-${discordId}-${botId}`; fs.mkdirSync(tempDir, { recursive: true }); fs.writeFileSync(path.join(tempDir, '.env'), envFile); fs.writeFileSync(path.join(tempDir, 'zbr.json'), zbrJson); const commandsDir = path.join(tempDir, 'commands'); fs.mkdirSync(commandsDir, { recursive: true }); commands.forEach(cmd => { fs.writeFileSync(path.join(commandsDir, cmd.filename), cmd.content); }); fs.writeFileSync(path.join(tempDir, 'zbr.db'), Buffer.from(dbBase64, 'base64')); const child = spawn('/usr/local/bin/zbr', [], { cwd: tempDir, stdio: ['ignore', 'pipe', 'pipe'] }); if (child.stdout) { child.stdout.on('data', (data) => console.log(data.toString())); } if (child.stderr) { child.stderr.on('data', (data) => console.error(data.toString())); } processes.set(key, { child, tempDir, startedAt: Date.now() }); setTimeout(() => { if (processes.has(key)) stopBot(key); }, 2 * 60 * 60 * 1000); res.json({ success: true, status: 'running' }); }); function stopBot(key: string): boolean { const proc = processes.get(key); if (!proc) return false; proc.child.kill(); fs.rmSync(proc.tempDir, { recursive: true, force: true }); processes.delete(key); return true; } app.post('/stop', (req: Request<{}, any, StopRequestBody>, res: Response) => { const { botId, discordId } = req.body; stopBot(`${discordId}-${botId}`); res.json({ success: true, status: 'stopped' }); }); app.get('/status/:discordId/:botId', (req: Request, res: Response) => { const { discordId, botId } = req.params; const proc = processes.get(`${discordId}-${botId}`); if (proc) { res.json({ status: 'running', startedAt: proc.startedAt }); } else { res.json({ status: 'stopped' }); } }); app.get('/health', (req: Request, res: Response) => res.json({ ok: true })); app.listen(7860, () => console.log('Server running on port 7860'));