haxball-server
Version:
A feature-rich and stable headless server utility for Haxball
359 lines • 17.5 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ControlPanel = void 0;
const node_os_utils_1 = __importDefault(require("node-os-utils"));
const fs_1 = __importDefault(require("fs"));
const pidusage_1 = __importDefault(require("pidusage"));
const process_1 = __importDefault(require("process"));
const Discord = __importStar(require("discord.js"));
const loadConfig_1 = require("./utils/loadConfig");
const log_1 = require("./utils/log");
class Bot {
constructor(name, path, displayName) {
this.name = name;
this.path = path;
this.displayName = displayName;
}
read() {
return new Promise((resolve, reject) => {
fs_1.default.readFile(this.path, { encoding: 'utf-8' }, async (err, data) => {
if (err) {
reject(err);
}
else {
resolve(data);
}
});
});
}
run(server, data, tokens, settings) {
return new Promise((resolve, reject) => {
server.open(data, tokens, this.displayName, settings)
.then(e => resolve(e))
.catch(err => reject(err));
});
}
}
class ControlPanel {
constructor(server, config, fileName) {
this.server = server;
this.fileName = fileName;
this.client = new Discord.Client();
this.cpu = node_os_utils_1.default.cpu;
this.mem = node_os_utils_1.default.mem;
this.bots = [];
this.prefix = config.discordPrefix;
this.token = config.discordToken;
this.mastersDiscordId = config.mastersDiscordId;
this.maxRooms = config.maxRooms;
if (config.customSettings)
this.loadCustomSettings(config.customSettings);
this.loadBots(config.bots);
this.client.on('ready', () => {
var _a;
(0, log_1.log)("DISCORD", `Logged in as ${(_a = this.client.user) === null || _a === void 0 ? void 0 : _a.tag}!`);
});
this.client.on('message', async (msg) => {
try {
this.command(msg);
}
catch (e) {
this.logError(e, msg.channel);
}
});
this.client.login(this.token);
}
transformSetting(setting, list) {
if (setting.extends) {
const extensions = typeof setting.extends === "string" ? [setting.extends] : setting.extends;
let newSetting = {};
for (const e of extensions) {
let ext = list[e];
if (ext) {
if (ext.extends)
ext = this.transformSetting(ext, list);
newSetting = Object.assign(Object.assign({}, newSetting), ext);
}
}
newSetting = Object.assign(Object.assign({}, newSetting), setting);
delete newSetting.extends;
return newSetting;
}
return setting;
}
loadCustomSettings(customSettings) {
this.customSettings = undefined;
for (const entry of Object.entries(customSettings)) {
const key = entry[0];
const value = entry[1];
customSettings[key] = this.transformSetting(value, customSettings);
}
this.customSettings = customSettings;
}
loadBots(bots) {
this.bots = [];
if (!Array.isArray(bots)) {
for (const entry of Object.entries(bots)) {
const name = entry[0];
const path = entry[1];
this.bots.push(new Bot(name, path));
}
}
else {
for (const bot of bots) {
this.bots.push(new Bot(bot.name, bot.path, bot.displayName));
}
}
}
async logError(e, channel) {
const embed = new Discord.MessageEmbed()
.setColor('#0099ff')
.setTitle("Log Error")
.setTimestamp(Date.now())
.setDescription(e);
await channel.send(embed);
}
async getRoomNameList() {
var _a;
let rooms = [];
for (const browser of this.server.browsers) {
const page = (await browser.pages())[0];
const proxyServer = browser["proxyServer"];
const remotePort = browser["remotePort"];
let pageTitle = await page.title();
pageTitle = pageTitle != null && pageTitle != "" ? pageTitle : "Unnamed tab";
const nameStr = `${pageTitle} (${(_a = page.browser().process()) === null || _a === void 0 ? void 0 : _a.pid})${remotePort != null ? ` (localhost:${remotePort})` : ""}`;
rooms.push({ name: nameStr, proxy: proxyServer });
}
if (rooms.length === 0)
return "There are no open rooms!";
if (rooms.every(r => r.proxy == null))
return rooms.map(r => r.name).join("\n");
let proxyRooms = [];
for (const room of rooms) {
let pRoom = proxyRooms.find(r => r.proxy === room.proxy);
if (pRoom) {
pRoom.text += room.name + "\n";
}
else {
proxyRooms.push({ text: room.name + "\n", proxy: room.proxy });
}
}
return proxyRooms.map(r => `• ${r.proxy}\n${r.text}`).join("\n");
}
async getRoomUsageList() {
var _a;
const roomsUsage = [];
for (const browser of this.server.browsers) {
const page = (await browser.pages())[0];
roomsUsage.push({ process: await (0, pidusage_1.default)((_a = browser === null || browser === void 0 ? void 0 : browser.process()) === null || _a === void 0 ? void 0 : _a.pid), title: await page.title() });
}
return roomsUsage;
}
async command(msg) {
var _a, _b;
if (!msg.content.startsWith(this.prefix))
return;
const args = msg.content.slice(this.prefix.length).trim().split(' ');
const text = msg.content.slice(this.prefix.length).trim().replace(args[0] + " ", "");
const command = (_a = args.shift()) === null || _a === void 0 ? void 0 : _a.toLowerCase();
const embed = new Discord.MessageEmbed().setColor('#0099ff');
if (this.mastersDiscordId.includes(msg.author.id)) {
if (command === "help") {
embed
.setTitle("Help")
.setDescription("Haxball Server is a small server utility for Haxball rooms.")
.addField("help", "Command list.", true)
.addField("info", "Server info.", true)
.addField("meminfo", "CPU and memory info.", true)
.addField("open", "Open a room.", true)
.addField("close", "Close a room.", true)
.addField("reload", "Reload the bot configuration.", true)
.addField("exit", "Close the server.", true)
.addField("eval", "Execute Javascript.", true)
.addField("tokenlink", "Haxball Headless Token page.", true);
msg.channel.send(embed);
}
if (command === "tokenlink") {
embed
.setTitle("Headless Token")
.setDescription(`[Click here.](https://www.haxball.com/headlesstoken)`);
msg.channel.send(embed);
}
if (command === "open") {
embed.setTitle("Open room");
if (this.maxRooms != null && this.server.browsers.length >= this.maxRooms) {
embed.setDescription(`Maximum number of rooms (${this.maxRooms}) excedeed. Update configuration to change this.`);
return msg.channel.send(embed);
}
const bot = this.bots.find(b => b.name === args[0]);
if (!bot) {
embed.setDescription(`This bot does not exist. Type ${this.prefix}info to see the list of available bots.`);
return msg.channel.send(embed);
}
let token = text.replace(args[0], "").trim().replace(/\"/g, "").replace("Token obtained: ", "");
if (!token || token === "") {
embed.setDescription(`You have to define a [headless token](https://www.haxball.com/headlesstoken) as second argument: ${this.prefix}open <bot> <token>`);
return msg.channel.send(embed);
}
let settings;
let settingsMsg = "No setting has been loaded (not specified or not found).";
if (this.customSettings != null) {
const settingArg = args[args.length - 1];
settings = this.customSettings[settingArg];
if (settings) {
settingsMsg = `\`${settingArg}\` settings have been loaded.`;
token = token.replace(settingArg, "").trim();
}
else if (this.customSettings["default"]) {
settingsMsg = `Default settings have been loaded.`;
settings = this.customSettings["default"];
}
}
embed.setDescription("Opening room...");
const message = await msg.channel.send(embed);
bot.read().then(script => {
bot.run(this.server, script, [token, token.substring(0, token.lastIndexOf(" "))], settings).then(e => {
message.edit(embed.setDescription(`Room running! [Click here to join.](${e === null || e === void 0 ? void 0 : e.link})\nBrowser process: ${e === null || e === void 0 ? void 0 : e.pid}${(e === null || e === void 0 ? void 0 : e.remotePort) ? `\nRemote debugging: localhost:${e.remotePort}` : ""}\n${settingsMsg}`));
})
.catch(err => {
message.edit(embed.setDescription(`Unable to open the room!\n ${err}`));
});
})
.catch(err => {
embed.setDescription("Error: " + err);
message.edit(embed);
});
}
if (command === "info") {
const roomList = await this.getRoomNameList();
embed
.setTitle("Information")
.addField("Open rooms", roomList)
.addField("Bot list", this.bots.map(b => b.name).join("\n"))
.addField("Custom settings list", this.customSettings ? Object.keys(this.customSettings).join("\n") : "No custom settings have been specified.");
msg.channel.send(embed);
}
if (command === "meminfo") {
const embedLoading = new Discord.MessageEmbed()
.setColor('#0099ff')
.setTitle("Information")
.setDescription("Loading...");
const message = await msg.channel.send(embedLoading);
const roomsUsage = await this.getRoomUsageList();
const memInfo = await this.mem.info();
const cpuUsage = await this.cpu.usage();
embed
.setTitle("Information")
.addField("CPUs", this.cpu.count(), true)
.addField("CPU usage", cpuUsage + "%", true)
.addField("Free CPU", 100 - cpuUsage + "%", true)
.addField("Memory", `${(memInfo.usedMemMb / 1000).toFixed(2)}/${(memInfo.totalMemMb / 1000).toFixed(2)} GB (${memInfo.freeMemPercentage}% livre)`, true)
.addField("OS", await node_os_utils_1.default.os.oos(), true)
.addField("Machine Uptime", new Date(node_os_utils_1.default.os.uptime() * 1000).toISOString().substr(11, 8), true);
const serverPIDUsage = await (0, pidusage_1.default)(process_1.default.pid);
const serverCPUUsage = `CPU server usage: ${(serverPIDUsage.cpu).toFixed(2)}%\nMemory server usage: ${(serverPIDUsage.memory * 1e-6).toFixed(2)} MB\n`;
const roomCPUMessage = this.server.browsers.length > 0 ? "\n" + roomsUsage.map((room) => `**${room.title} (${room.process.pid})**:\n${(room.process.cpu).toFixed(2)}% CPU\n${(room.process.memory * 1e-6).toFixed(2)} MB memory\n`).join("\n") : "";
embed.setDescription(serverCPUUsage + roomCPUMessage + "\n");
message.edit(embed);
}
if (command === "close") {
embed
.setTitle("Close room")
.setDescription("Unable to find room");
if (args[0] === "all") {
let forcedClosedRooms = 0;
let closedRooms = 0;
for (const room of this.server.browsers) {
const pid = (_b = room === null || room === void 0 ? void 0 : room.process()) === null || _b === void 0 ? void 0 : _b.pid;
if (pid) {
await this.server.close(pid);
}
else {
await room.close();
forcedClosedRooms++;
}
closedRooms++;
}
if (forcedClosedRooms === 0) {
embed.setDescription(`${closedRooms} rooms have been closed.`);
}
else {
embed.setDescription(`${closedRooms} rooms have been closed.\n${forcedClosedRooms} rooms have been forced to close.`);
}
return msg.channel.send(embed);
}
const res = await this.server.close(text);
if (res)
embed.setDescription("Room closed!");
msg.channel.send(embed);
}
if (command === "exit") {
embed
.setTitle("Closing")
.setDescription("Closing server...");
await msg.channel.send(embed);
this.server.browsers.forEach(async (browser) => {
await browser.close();
});
process_1.default.exit();
}
if (command === "eval") {
try {
const code = args.join(" ");
let evaled = eval(code);
if (typeof evaled !== "string")
evaled = require("util").inspect(evaled);
msg.channel.send(evaled, { code: "javascript", split: true });
}
catch (err) {
msg.channel.send(`\`ERROR\` \`\`\`xl\n${err}\n\`\`\``);
}
}
if (command === "reload") {
embed.setTitle("Reload bots and custom settings").setColor(0xFF0000);
(0, loadConfig_1.loadConfig)(this.fileName).then((config) => {
if (!config.panel.bots) {
embed.setDescription("Could not find bots in config file.");
}
else {
this.loadBots(config.panel.bots);
if (config.panel.customSettings)
this.loadCustomSettings(config.panel.customSettings);
this.maxRooms = config.panel.maxRooms;
embed.setColor(0x0099FF).setDescription("Bot list and custom settings reloaded!");
}
msg.channel.send(embed);
}).catch(err => {
embed.setDescription(`*${err.message}*\n\nSee logs for details.`);
console.error(err);
msg.channel.send(embed);
});
}
}
}
}
exports.ControlPanel = ControlPanel;
//# sourceMappingURL=ControlPanel.js.map