360 lines
16 KiB
JavaScript
360 lines
16 KiB
JavaScript
const express = require('express');
|
|||
|
|
const cors = require('cors');
|
||
|
|
const fs = require('fs');
|
||
|
|
const os = require('os');
|
||
|
|
const yaml = require('js-yaml');
|
||
|
|
const YAML = require('yaml');
|
||
|
|
const db = require('../Structures/Database.js');
|
||
|
|
|
||
|
|
class APIServer {
|
||
|
|
constructor(client) {
|
||
|
|
this.client = client;
|
||
|
|
this.app = express();
|
||
|
|
|
||
|
|
try {
|
||
|
|
const apiCfg = yaml.load(fs.readFileSync('./Configs/api.yml', 'utf8')).API;
|
||
|
|
this.config = apiCfg;
|
||
|
|
} catch (e) {
|
||
|
|
console.error('[API] Failed to load api.yml, defaulting to disabled.');
|
||
|
|
this.config = { Enabled: false };
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!this.config.Enabled) return;
|
||
|
|
|
||
|
|
this.app.use(cors());
|
||
|
|
this.app.use(express.json());
|
||
|
|
|
||
|
|
this.app.use((req, res, next) => {
|
||
|
|
const authHeader = req.headers.authorization;
|
||
|
|
if (!authHeader || authHeader !== `Bearer ${this.config.SecretKey}`) {
|
||
|
|
return res.status(401).json({ error: 'Unauthorized. Invalid Secret Key.' });
|
||
|
|
}
|
||
|
|
next();
|
||
|
|
});
|
||
|
|
|
||
|
|
this.setupRoutes();
|
||
|
|
}
|
||
|
|
|
||
|
|
start(port) {
|
||
|
|
const listenPort = port || this.config.Port || 3000;
|
||
|
|
this.app.listen(listenPort, () => {
|
||
|
|
console.log(`[API] Server running on port ${listenPort}`);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
setupRoutes() {
|
||
|
|
this.app.get('/api/stats', async (req, res) => {
|
||
|
|
try {
|
||
|
|
if (!this.client.user) {
|
||
|
|
return res.json({ success: false, error: 'Bot is still starting up.' });
|
||
|
|
}
|
||
|
|
|
||
|
|
let userCount = 0;
|
||
|
|
this.client.guilds.cache.forEach(guild => {
|
||
|
|
userCount += guild.memberCount;
|
||
|
|
});
|
||
|
|
|
||
|
|
const allTickets = db.getAllTickets() || [];
|
||
|
|
const openTickets = allTickets.filter(t => t.open).length;
|
||
|
|
const closedTickets = allTickets.filter(t => !t.open).length;
|
||
|
|
|
||
|
|
const totalMem = os.totalmem();
|
||
|
|
const freeMem = os.freemem();
|
||
|
|
const usedMem = totalMem - freeMem;
|
||
|
|
const ramPercent = Math.round((usedMem / totalMem) * 100);
|
||
|
|
const cpuLoad = os.loadavg()[0].toFixed(2);
|
||
|
|
|
||
|
|
res.json({
|
||
|
|
success: true,
|
||
|
|
data: {
|
||
|
|
bot: {
|
||
|
|
username: this.client.user.username,
|
||
|
|
id: this.client.user.id,
|
||
|
|
avatar: this.client.user.displayAvatarURL(),
|
||
|
|
version: require('../package.json').version,
|
||
|
|
ping: this.client.ws.ping,
|
||
|
|
uptime: process.uptime(),
|
||
|
|
},
|
||
|
|
servers: this.client.guilds.cache.size,
|
||
|
|
users: userCount,
|
||
|
|
tickets: {
|
||
|
|
total: allTickets.length,
|
||
|
|
open: openTickets,
|
||
|
|
closed: closedTickets
|
||
|
|
},
|
||
|
|
hosting: {
|
||
|
|
ram_used: (usedMem / 1024 / 1024 / 1024).toFixed(2) + ' GB',
|
||
|
|
ram_total: (totalMem / 1024 / 1024 / 1024).toFixed(2) + ' GB',
|
||
|
|
ram_percent: ramPercent,
|
||
|
|
cpu_load: cpuLoad,
|
||
|
|
uptime: Math.floor(os.uptime() / 86400) + ' Days'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
} catch (err) {
|
||
|
|
console.error('[API] Error fetching stats:', err);
|
||
|
|
res.status(500).json({ success: false, error: 'Internal Server Error' });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.app.get('/api/configs/:file', (req, res) => {
|
||
|
|
const validFiles = ['supportbot', 'ticket-panel', 'commands', 'messages', 'supportbot-ai'];
|
||
|
|
const file = req.params.file;
|
||
|
|
if (!validFiles.includes(file)) return res.status(400).json({ success: false, error: 'Invalid file' });
|
||
|
|
try {
|
||
|
|
const configData = fs.readFileSync(`./Configs/${file}.yml`, 'utf8');
|
||
|
|
res.json({ success: true, data: configData });
|
||
|
|
} catch (err) {
|
||
|
|
res.status(500).json({ success: false, error: 'Failed to read config' });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.app.put('/api/configs/raw', (req, res) => {
|
||
|
|
const validFiles = ['supportbot', 'ticket-panel', 'commands', 'messages', 'supportbot-ai'];
|
||
|
|
const file = req.body.filename;
|
||
|
|
const content = req.body.content;
|
||
|
|
if (!validFiles.includes(file)) return res.status(400).json({ success: false, error: 'Invalid file' });
|
||
|
|
try {
|
||
|
|
fs.writeFileSync(`./Configs/${file}.yml`, content);
|
||
|
|
res.json({ success: true });
|
||
|
|
} catch (err) {
|
||
|
|
res.status(500).json({ success: false, error: 'Failed to write config' });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.app.get('/api/configs/json/:file', (req, res) => {
|
||
|
|
const validFiles = ['supportbot', 'ticket-panel', 'commands', 'messages', 'supportbot-ai'];
|
||
|
|
const file = req.params.file;
|
||
|
|
if (!validFiles.includes(file)) return res.status(400).json({ success: false, error: 'Invalid file' });
|
||
|
|
try {
|
||
|
|
const configData = yaml.load(fs.readFileSync(`./Configs/${file}.yml`, 'utf8'));
|
||
|
|
res.json({ success: true, data: configData });
|
||
|
|
} catch (err) {
|
||
|
|
res.status(500).json({ success: false, error: 'Failed to parse config' });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.app.post('/api/configs/update-fields/multi', (req, res) => {
|
||
|
|
const updates = req.body;
|
||
|
|
if (!updates || typeof updates !== 'object') return res.status(400).json({ success: false, error: 'Invalid updates' });
|
||
|
|
try {
|
||
|
|
for (const [keyPath, value] of Object.entries(updates)) {
|
||
|
|
const [fileName, path] = keyPath.split(':');
|
||
|
|
const fileContent = fs.readFileSync(`./Configs/${fileName}.yml`, 'utf8');
|
||
|
|
const doc = YAML.parseDocument(fileContent);
|
||
|
|
const keys = path.split('.');
|
||
|
|
doc.setIn(keys, value);
|
||
|
|
fs.writeFileSync(`./Configs/${fileName}.yml`, doc.toString());
|
||
|
|
}
|
||
|
|
res.json({ success: true });
|
||
|
|
} catch (err) {
|
||
|
|
console.error('[API] Update failed:', err);
|
||
|
|
res.status(500).json({ success: false, error: err.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.app.post('/api/addons/install', async (req, res) => {
|
||
|
|
const { url, filename } = req.body;
|
||
|
|
if (!url || !filename) return res.status(400).json({ success: false, error: 'Missing url or filename' });
|
||
|
|
|
||
|
|
try {
|
||
|
|
const axios = require('axios');
|
||
|
|
const response = await axios({
|
||
|
|
method: 'get',
|
||
|
|
url: url,
|
||
|
|
responseType: 'stream'
|
||
|
|
});
|
||
|
|
|
||
|
|
// Ensure Addons directory exists
|
||
|
|
if (!fs.existsSync('./Addons')) {
|
||
|
|
fs.mkdirSync('./Addons');
|
||
|
|
}
|
||
|
|
|
||
|
|
const writer = fs.createWriteStream(`./Addons/${filename}`);
|
||
|
|
response.data.pipe(writer);
|
||
|
|
|
||
|
|
writer.on('finish', () => {
|
||
|
|
res.json({ success: true, message: `Addon ${filename} installed successfully.` });
|
||
|
|
});
|
||
|
|
|
||
|
|
writer.on('error', (err) => {
|
||
|
|
console.error('[API] Download failed:', err);
|
||
|
|
res.status(500).json({ success: false, error: 'Failed to write addon file' });
|
||
|
|
});
|
||
|
|
|
||
|
|
} catch (err) {
|
||
|
|
console.error('[API] Addon install failed:', err);
|
||
|
|
res.status(500).json({ success: false, error: 'Failed to download addon' });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.app.post('/api/addons/push', (req, res) => {
|
||
|
|
const { filename, content } = req.body;
|
||
|
|
if (!filename || !content) return res.status(400).json({ success: false, error: 'Missing filename or content' });
|
||
|
|
|
||
|
|
const sanitizedFilename = filename.replace(/[^a-z0-9_.-]/gi, '_');
|
||
|
|
|
||
|
|
try {
|
||
|
|
if (!fs.existsSync('./Addons')) {
|
||
|
|
fs.mkdirSync('./Addons');
|
||
|
|
}
|
||
|
|
fs.writeFileSync(`./Addons/${sanitizedFilename}`, content);
|
||
|
|
res.json({ success: true, message: `Addon ${sanitizedFilename} deployed successfully.` });
|
||
|
|
} catch (err) {
|
||
|
|
console.error('[API] Push failed:', err);
|
||
|
|
res.status(500).json({ success: false, error: 'Failed to write addon file' });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// --- Nexus Updater API ---
|
||
|
|
this.app.post('/api/system/update', async (req, res) => {
|
||
|
|
const { url, version } = req.body;
|
||
|
|
if (!url) return res.status(400).json({ success: false, error: 'Missing update URL' });
|
||
|
|
|
||
|
|
try {
|
||
|
|
const axios = require('axios');
|
||
|
|
const { execSync } = require('child_process');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
console.log(`[Updater] Starting update to ${version || 'latest'}...`);
|
||
|
|
|
||
|
|
// 1. Download
|
||
|
|
const tempZip = `./update_${Date.now()}.zip`;
|
||
|
|
const response = await axios({ method: 'get', url: url, responseType: 'stream' });
|
||
|
|
const writer = fs.createWriteStream(tempZip);
|
||
|
|
response.data.pipe(writer);
|
||
|
|
|
||
|
|
await new Promise((resolve, reject) => {
|
||
|
|
writer.on('finish', resolve);
|
||
|
|
writer.on('error', reject);
|
||
|
|
});
|
||
|
|
|
||
|
|
// 2. Extract to temp dir
|
||
|
|
const tempDir = `./temp_update_${Date.now()}`;
|
||
|
|
if (!fs.existsSync(tempDir)) fs.mkdirSync(tempDir);
|
||
|
|
|
||
|
|
const isWindows = process.platform === 'win32';
|
||
|
|
try {
|
||
|
|
if (isWindows) {
|
||
|
|
execSync(`powershell -Command "Expand-Archive -Path '${tempZip}' -DestinationPath '${tempDir}' -Force"`);
|
||
|
|
} else {
|
||
|
|
execSync(`unzip -o "${tempZip}" -d "${tempDir}"`);
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
throw new Error(`Extraction failed. Ensure ${isWindows ? 'PowerShell' : 'unzip'} is installed. ${e.message}`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Identify inner directory (sometimes zips contain a root folder)
|
||
|
|
let sourcePath = tempDir;
|
||
|
|
const items = fs.readdirSync(tempDir);
|
||
|
|
if (items.length === 1 && fs.statSync(path.join(tempDir, items[0])).isDirectory()) {
|
||
|
|
sourcePath = path.join(tempDir, items[0]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. Smart Config Merge
|
||
|
|
const configFiles = ['supportbot', 'commands', 'messages', 'ticket-panel', 'supportbot-ai'];
|
||
|
|
configFiles.forEach(cf => {
|
||
|
|
const currentPath = `./Configs/${cf}.yml`;
|
||
|
|
const newPath = path.join(sourcePath, 'Configs', `${cf}.yml`);
|
||
|
|
|
||
|
|
if (fs.existsSync(currentPath) && fs.existsSync(newPath)) {
|
||
|
|
const currentCfg = yaml.load(fs.readFileSync(currentPath, 'utf8'));
|
||
|
|
const newCfg = yaml.load(fs.readFileSync(newPath, 'utf8'));
|
||
|
|
|
||
|
|
const merge = (target, source) => {
|
||
|
|
for (const key of Object.keys(source)) {
|
||
|
|
if (source[key] instanceof Object && !Array.isArray(source[key]) && target[key]) {
|
||
|
|
merge(target[key], source[key]);
|
||
|
|
} else if (target[key] === undefined) {
|
||
|
|
target[key] = source[key];
|
||
|
|
console.log(`[Updater] Added missing key: ${key} to ${cf}.yml`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
merge(currentCfg, newCfg);
|
||
|
|
fs.writeFileSync(currentPath, YAML.stringify(currentCfg));
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 4. Overwrite Core Folders
|
||
|
|
const coreFolders = ['API', 'Commands', 'Events', 'Structures'];
|
||
|
|
coreFolders.forEach(folder => {
|
||
|
|
const src = path.join(sourcePath, folder);
|
||
|
|
if (fs.existsSync(src)) {
|
||
|
|
if (isWindows) {
|
||
|
|
execSync(`xcopy "${src}" ".\\${folder}" /E /I /Y`);
|
||
|
|
} else {
|
||
|
|
execSync(`cp -R "${src}/"* "./${folder}/"`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// 5. Update package.json (version only)
|
||
|
|
const newPkgPath = path.join(sourcePath, 'package.json');
|
||
|
|
if (fs.existsSync(newPkgPath)) {
|
||
|
|
const currentPkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
|
||
|
|
const newPkg = JSON.parse(fs.readFileSync(newPkgPath, 'utf8'));
|
||
|
|
currentPkg.version = newPkg.version;
|
||
|
|
fs.writeFileSync('./package.json', JSON.stringify(currentPkg, null, 2));
|
||
|
|
}
|
||
|
|
|
||
|
|
fs.unlinkSync(tempZip);
|
||
|
|
if (isWindows) {
|
||
|
|
execSync(`rmdir /s /q "${tempDir}"`);
|
||
|
|
} else {
|
||
|
|
execSync(`rm -rf "${tempDir}"`);
|
||
|
|
}
|
||
|
|
|
||
|
|
res.json({ success: true, message: 'Update installed successfully. Bot is restarting...' });
|
||
|
|
|
||
|
|
// Reboot
|
||
|
|
setTimeout(() => process.exit(0), 1000);
|
||
|
|
|
||
|
|
} catch (err) {
|
||
|
|
console.error('[Updater] Update failed:', err);
|
||
|
|
res.status(500).json({ success: false, error: err.message });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// --- Transcript API ---
|
||
|
|
this.app.get('/api/system/transcripts', (req, res) => {
|
||
|
|
const transcriptDir = './Data/Transcripts';
|
||
|
|
try {
|
||
|
|
if (!fs.existsSync(transcriptDir)) {
|
||
|
|
return res.json({ success: true, data: [] });
|
||
|
|
}
|
||
|
|
const files = fs.readdirSync(transcriptDir);
|
||
|
|
const transcripts = files
|
||
|
|
.filter(f => f.endsWith('-transcript.html'))
|
||
|
|
.map(f => {
|
||
|
|
const stats = fs.statSync(`${transcriptDir}/${f}`);
|
||
|
|
return {
|
||
|
|
id: f.replace('-transcript.html', ''),
|
||
|
|
filename: f,
|
||
|
|
createdAt: stats.mtime
|
||
|
|
};
|
||
|
|
})
|
||
|
|
.sort((a, b) => b.createdAt - a.createdAt);
|
||
|
|
res.json({ success: true, data: transcripts });
|
||
|
|
} catch (err) {
|
||
|
|
res.status(500).json({ success: false, error: 'Failed to list transcripts' });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
this.app.get('/api/system/transcripts/:id', (req, res) => {
|
||
|
|
const id = req.params.id.replace(/[^0-9]/g, ''); // Sanitize ID
|
||
|
|
const filePath = `./Data/Transcripts/${id}-transcript.html`;
|
||
|
|
try {
|
||
|
|
if (!fs.existsSync(filePath)) {
|
||
|
|
return res.status(404).json({ success: false, error: 'Transcript not found' });
|
||
|
|
}
|
||
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
||
|
|
res.json({ success: true, data: content });
|
||
|
|
} catch (err) {
|
||
|
|
res.status(500).json({ success: false, error: 'Failed to read transcript' });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = APIServer;
|