Update package version v5.1.2 #472
101
README.md
101
README.md
@@ -10,104 +10,3 @@ A quick and simple way to create and manage Minecraft servers like `Bedrock`, `J
|
||||
## Wiki
|
||||
|
||||
We are moving the documentation [here](https://wiki.bdsmaneger.sirherobrine23.org/).
|
||||
|
||||
## Example
|
||||
|
||||
With yargs and cli-color ([This is the cli](https://github.com/The-Bds-Maneger/bds-cli)).
|
||||
|
||||
```typescript
|
||||
#!/usr/bin/env node
|
||||
import readline from "node:readline";
|
||||
import yargs from "yargs";
|
||||
import cliColors from "cli-color";
|
||||
import * as BdsCore from "@the-bds-maneger/core";
|
||||
|
||||
// type currentPlatform = "Bedrock"|"Java"|"Spigot"|"PocketmineMP"|"Powernukkit";
|
||||
const Yargs = yargs(process.argv.slice(2)).help().version(false).alias("h", "help").wrap(yargs.terminalWidth());
|
||||
|
||||
// Bds import/export
|
||||
Yargs.command("import", "import from another computer", async yargs => {
|
||||
const options = yargs.options("host", {
|
||||
type: "string"
|
||||
}).options("port", {
|
||||
type: "number"
|
||||
}).option("authToken", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
required: true
|
||||
}).parseSync();
|
||||
return BdsCore.importBds({host: options.host, port: options.port, authToken: options.authToken});
|
||||
}).command("export", "Export bds root folder", async yargs => {
|
||||
const opts = yargs.option("port", {
|
||||
type: "number",
|
||||
description: "listen port server, default is random"
|
||||
}).parseSync();
|
||||
const server = new BdsCore.exportBds();
|
||||
await server.listen(opts.port);
|
||||
return server.waitClose();
|
||||
});
|
||||
|
||||
const addPlatform = (Yargs) => Yargs.option("platform", {
|
||||
alias: "P",
|
||||
description: "Select Bds Maneger core platform",
|
||||
demandOption: true,
|
||||
type: "string",
|
||||
choices: [
|
||||
"Bedrock",
|
||||
"Java",
|
||||
"Spigot",
|
||||
"PocketmineMP",
|
||||
"Powernukkit",
|
||||
"Paper"
|
||||
]
|
||||
});
|
||||
|
||||
// Install Server
|
||||
Yargs.command("install", "Download and Install server", yargs => {
|
||||
addPlatform(yargs);
|
||||
const options = yargs.option("version", {alias: "v", description: "Server version", default: "latest"}).parseSync();
|
||||
if (options.platform === "Bedrock") return BdsCore.Bedrock.installServer(options.version);
|
||||
else if (options.platform === "Java") return BdsCore.Java.installServer(options.version);
|
||||
else if (options.platform === "Spigot") return BdsCore.Spigot.installServer(options.version);
|
||||
else if (options.platform === "Paper") return BdsCore.PaperMC.installServer(options.version);
|
||||
else if (options.platform === "PocketmineMP") return BdsCore.PocketmineMP.installServer(options.version);
|
||||
else if (options.platform === "Powernukkit") return BdsCore.Powernukkit.installServer(options.version);
|
||||
throw new Error("Invalid platform");
|
||||
});
|
||||
|
||||
// Start Server
|
||||
Yargs.command("run", "Start server", async yargs => {
|
||||
addPlatform(yargs);
|
||||
const options = yargs.option("javaMaxMemory", {alias: "M", type: "number"}).option("javaAllFreeMem", {alias: "F", type: "boolean", default: true, description: "Use all free memory to run Java server"}).option("installGeyser", {alias: "g", type: "boolean", description: "Install geyser plugin to supported servers"}).parseSync();
|
||||
let server: BdsCore.globalPlatfroms.actions;
|
||||
if (options.platform === "Bedrock") server = await BdsCore.Bedrock.startServer();
|
||||
else if (options.platform === "Java") server = await BdsCore.Java.startServer({maxMemory: options.javaMaxMemory});
|
||||
else if (options.platform === "Spigot") server = await BdsCore.Spigot.startServer({maxMemory: options.javaMaxMemory, maxFreeMemory: options.javaAllFreeMem, pluginList: options.installGeyser ? ["Geyser"]:undefined});
|
||||
else if (options.platform === "Paper") server = await BdsCore.PaperMC.startServer({maxMemory: options.javaMaxMemory, maxFreeMemory: options.javaAllFreeMem, pluginList: options.installGeyser ? ["Geyser"]:undefined});
|
||||
else if (options.platform === "PocketmineMP") server = await BdsCore.PocketmineMP.startServer();
|
||||
else if (options.platform === "Powernukkit") server = await BdsCore.Powernukkit.startServer({maxMemory: options.javaMaxMemory});
|
||||
else throw new Error("Invalid platform");
|
||||
server.on("log_stderr", data => console.log(cliColors.redBright(data)));
|
||||
server.on("log_stdout", data => console.log(cliColors.greenBright(data)));
|
||||
server.once("serverStarted", () => {
|
||||
let log = "";
|
||||
server.portListening.forEach(port => {
|
||||
log += `Port listen: ${port.port}\n${"\tTo: "+(port.plugin === "geyser"?"Bedrock":(port.plugin||options.platform)+"\n")}`;
|
||||
});
|
||||
console.log(log.trim());
|
||||
console.log(cliColors.yellowBright("Commands inputs now avaible"))
|
||||
const line = readline.createInterface({input: process.stdin, output: process.stdout});
|
||||
line.on("line", line => server.runCommand(line));
|
||||
line.once("SIGINT", () => server.stopServer());
|
||||
line.once("SIGCONT", () => server.stopServer());
|
||||
line.once("SIGTSTP", () => server.stopServer());
|
||||
server.once("exit", () => line.close());
|
||||
});
|
||||
return server.waitExit();
|
||||
});
|
||||
|
||||
// Plugin maneger
|
||||
// Yargs.command("plugin", "Plugin maneger", yargs=>yargs.command("install", "Install plugin", yargs => yargs, () => {}).command({command: "*", handler: () => {Yargs.showHelp();}}), ()=>{});
|
||||
|
||||
Yargs.command({command: "*", handler: () => {Yargs.showHelp();}}).parseAsync();
|
||||
```
|
||||
|
||||
210
package-lock.json
generated
210
package-lock.json
generated
@@ -1,22 +1,19 @@
|
||||
{
|
||||
"name": "@the-bds-maneger/core",
|
||||
"version": "5.0.2",
|
||||
"version": "5.1.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@the-bds-maneger/core",
|
||||
"version": "5.0.2",
|
||||
"version": "5.1.1",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@the-bds-maneger/server_versions": "^4.2.0",
|
||||
"adm-zip": "^0.5.9",
|
||||
"cron": "^2.1.0",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.6.0",
|
||||
"got": "^12.5.2",
|
||||
"prismarine-nbt": "^2.2.1",
|
||||
"prom-client": "^14.1.0",
|
||||
"tar": "^6.1.11"
|
||||
},
|
||||
"bin": {
|
||||
@@ -25,7 +22,6 @@
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.0",
|
||||
"@types/cron": "^2.0.0",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/node": "^18.11.0",
|
||||
"@types/tar": "^6.1.3",
|
||||
@@ -155,25 +151,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
|
||||
"integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cron": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cron/-/cron-2.0.0.tgz",
|
||||
@@ -184,29 +161,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express": {
|
||||
"version": "4.17.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz",
|
||||
"integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^4.17.18",
|
||||
"@types/qs": "*",
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express-serve-static-core": {
|
||||
"version": "4.17.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz",
|
||||
"integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
"@types/range-parser": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-cache-semantics": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
||||
@@ -218,12 +172,6 @@
|
||||
"integrity": "sha512-/LAvk1cMOJt0ghzMFrZEvByUhsiEfeeT2IF53Le+Ki3A538yEL9pRZ7a6MuCxdrYK+YNqNIDmrKU/r2nnw04zQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
|
||||
"integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mocha": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.0.tgz",
|
||||
@@ -235,28 +183,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.0.tgz",
|
||||
"integrity": "sha512-IOXCvVRToe7e0ny7HpT/X9Rb2RYtElG1a+VshjwT00HxrM2dWBApHQoqsI6WiY7Q03vdf2bCrIGzVrkF/5t10w=="
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
|
||||
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz",
|
||||
"integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/mime": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tar": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.3.tgz",
|
||||
@@ -490,11 +416,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/bintrees": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
|
||||
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||
@@ -1105,17 +1026,6 @@
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.6.0.tgz",
|
||||
"integrity": "sha512-HFN2+4ZGdkQOS8Qli4z6knmJFnw6lZed67o6b7RGplWeb1Z0s8VXaj3dUgPIdm9hrhZXTRpCTHXA0/2Eqex0vA==",
|
||||
"engines": {
|
||||
"node": ">= 12.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": "^4 || ^5"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -2366,17 +2276,6 @@
|
||||
"protodef": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prom-client": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.1.0.tgz",
|
||||
"integrity": "sha512-iFWCchQmi4170omLpFXbzz62SQTmPhtBL35v0qGEVRHKcqIeiexaoYeP0vfZTujxEq3tA87iqOdRbC9svS1B9A==",
|
||||
"dependencies": {
|
||||
"tdigest": "^0.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/protodef": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/protodef/-/protodef-1.15.0.tgz",
|
||||
@@ -2805,14 +2704,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/tdigest": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
|
||||
"integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
|
||||
"dependencies": {
|
||||
"bintrees": "1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -3370,25 +3261,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/body-parser": {
|
||||
"version": "1.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz",
|
||||
"integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/cron": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cron/-/cron-2.0.0.tgz",
|
||||
@@ -3399,29 +3271,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/express": {
|
||||
"version": "4.17.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz",
|
||||
"integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^4.17.18",
|
||||
"@types/qs": "*",
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"@types/express-serve-static-core": {
|
||||
"version": "4.17.31",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz",
|
||||
"integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
"@types/range-parser": "*"
|
||||
}
|
||||
},
|
||||
"@types/http-cache-semantics": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
|
||||
@@ -3433,12 +3282,6 @@
|
||||
"integrity": "sha512-/LAvk1cMOJt0ghzMFrZEvByUhsiEfeeT2IF53Le+Ki3A538yEL9pRZ7a6MuCxdrYK+YNqNIDmrKU/r2nnw04zQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
|
||||
"integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mocha": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.0.tgz",
|
||||
@@ -3450,28 +3293,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.0.tgz",
|
||||
"integrity": "sha512-IOXCvVRToe7e0ny7HpT/X9Rb2RYtElG1a+VshjwT00HxrM2dWBApHQoqsI6WiY7Q03vdf2bCrIGzVrkF/5t10w=="
|
||||
},
|
||||
"@types/qs": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/range-parser": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
|
||||
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/serve-static": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz",
|
||||
"integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/mime": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/tar": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.3.tgz",
|
||||
@@ -3645,11 +3466,6 @@
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true
|
||||
},
|
||||
"bintrees": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz",
|
||||
"integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw=="
|
||||
},
|
||||
"body-parser": {
|
||||
"version": "1.20.1",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
|
||||
@@ -4088,12 +3904,6 @@
|
||||
"vary": "~1.1.2"
|
||||
}
|
||||
},
|
||||
"express-rate-limit": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.6.0.tgz",
|
||||
"integrity": "sha512-HFN2+4ZGdkQOS8Qli4z6knmJFnw6lZed67o6b7RGplWeb1Z0s8VXaj3dUgPIdm9hrhZXTRpCTHXA0/2Eqex0vA==",
|
||||
"requires": {}
|
||||
},
|
||||
"fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -4990,14 +4800,6 @@
|
||||
"protodef": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"prom-client": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/prom-client/-/prom-client-14.1.0.tgz",
|
||||
"integrity": "sha512-iFWCchQmi4170omLpFXbzz62SQTmPhtBL35v0qGEVRHKcqIeiexaoYeP0vfZTujxEq3tA87iqOdRbC9svS1B9A==",
|
||||
"requires": {
|
||||
"tdigest": "^0.1.1"
|
||||
}
|
||||
},
|
||||
"protodef": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/protodef/-/protodef-1.15.0.tgz",
|
||||
@@ -5319,14 +5121,6 @@
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"tdigest": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz",
|
||||
"integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==",
|
||||
"requires": {
|
||||
"bintrees": "1.0.2"
|
||||
}
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
|
||||
@@ -31,24 +31,17 @@
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"bdsd": "./src/bdsd.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@the-bds-maneger/server_versions": "^4.2.0",
|
||||
"adm-zip": "^0.5.9",
|
||||
"cron": "^2.1.0",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^6.6.0",
|
||||
"got": "^12.5.2",
|
||||
"prismarine-nbt": "^2.2.1",
|
||||
"prom-client": "^14.1.0",
|
||||
"tar": "^6.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.0",
|
||||
"@types/cron": "^2.0.0",
|
||||
"@types/express": "^4.17.14",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/node": "^18.11.0",
|
||||
"@types/tar": "^6.1.3",
|
||||
|
||||
207
src/bdsd.ts
207
src/bdsd.ts
@@ -1,207 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import os from "node:os";
|
||||
import fs from "node:fs";
|
||||
import fsPromise from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import crypto from "node:crypto";
|
||||
import express from "express";
|
||||
import expressRateLimit from "express-rate-limit";
|
||||
import * as bdsCore from "./index";
|
||||
import * as Prometheus from "prom-client";
|
||||
process.on("unhandledRejection", err => console.trace(err));
|
||||
Prometheus.collectDefaultMetrics({prefix: "bdsd"});
|
||||
const requests = new Prometheus.Counter({
|
||||
name: "bdsd_requests",
|
||||
help: "Total number of requests to the Server",
|
||||
labelNames: ["method", "from", "path"]
|
||||
});
|
||||
|
||||
const app = express();
|
||||
const bdsdAuth = path.join(bdsCore.platformPathManeger.bdsRoot, "bdsd_auth.json");
|
||||
const sockListen = path.join(os.tmpdir(), "bdsd.sock");
|
||||
if (fs.existsSync(sockListen)) fs.rmSync(sockListen, {force: true});
|
||||
app.disable("x-powered-by").disable("etag");
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({extended: true}));
|
||||
app.use(({ res, next }) => { res.json = (body: any) => res.setHeader("Content-Type", "application/json").send(JSON.stringify(body, null, 2)); return next(); });
|
||||
app.get("/metrics", async ({res, next}) => Prometheus.register.metrics().then(data => res.set("Content-Type", Prometheus.register.contentType).send(data)).catch(err => next(err)));
|
||||
app.use((req, _, next) => {requests.inc({method: req.method, path: req.path, from: !(req.socket.remoteAddress&&req.socket.remotePort)?"socket":req.protocol});next();});
|
||||
app.use(expressRateLimit({skipSuccessfulRequests: true, message: "Already reached the limit, please wait a few moments", windowMs: (1000*60)*2, max: 1500}));
|
||||
if (process.env.BDSD_IGNORE_KEY) console.warn("Bdsd ignore auth key!");
|
||||
app.use(async (req, res, next) => {
|
||||
// Allow by default socket
|
||||
if (!req.socket.remoteAddress && !req.socket.remotePort) {
|
||||
res.setHeader("AuthSocket", "true");
|
||||
return next();
|
||||
}
|
||||
|
||||
// External requests
|
||||
if (process.env.BDSD_IGNORE_KEY) return next();
|
||||
if (!fs.existsSync(bdsdAuth)) {
|
||||
if (!fs.existsSync(bdsCore.platformPathManeger.bdsRoot)) await fsPromise.mkdir(bdsCore.platformPathManeger.bdsRoot, {recursive: true});
|
||||
const keys = crypto.generateKeyPairSync("rsa", {modulusLength: 4096, publicKeyEncoding: {type: "spki", format: "pem"}, privateKeyEncoding: {type: "pkcs8", format: "pem", cipher: "aes-256-cbc", passphrase: crypto.randomBytes(128).toString("hex")}});
|
||||
await fsPromise.writeFile(bdsdAuth, JSON.stringify(keys, null, 2));
|
||||
console.log("Bdsd Keys\nPublic base64: '%s'\n\npublic:\n%s", Buffer.from(keys.publicKey).toString("base64"), keys.publicKey);
|
||||
return res.status(204).json({
|
||||
message: "Generated keys, re-auth with new public key!"
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.headers.authorization) return res.status(400).json({error: "Send authorization with public key!"});
|
||||
const authorizationPub = Buffer.from(req.headers.authorization.replace(/^.*\s+/, ""), "base64").toString("utf8").trim();
|
||||
const publicKey = (JSON.parse(await fsPromise.readFile(bdsdAuth, "utf8")) as crypto.KeyPairSyncResult<string, string>).publicKey.trim();
|
||||
if (publicKey === authorizationPub) return next();
|
||||
return res.status(400).json({
|
||||
error: "Invalid auth or incorret public key"
|
||||
});
|
||||
});
|
||||
|
||||
// Listen socks
|
||||
app.listen(sockListen, function () {console.info("Socket listen on '%s'", this.address());});
|
||||
if (process.env.PORT) app.listen(process.env.PORT, () => console.info("HTTP listen on http://127.0.0.1:%s", process.env.PORT));
|
||||
|
||||
let timesBefore = os.cpus().map(c => c.times);
|
||||
function getAverageUsage() {
|
||||
let timesAfter = os.cpus().map(c => c.times);
|
||||
let timeDeltas = timesAfter.map((t, i) => ({
|
||||
user: t.user - timesBefore[i].user,
|
||||
sys: t.sys - timesBefore[i].sys,
|
||||
idle: t.idle - timesBefore[i].idle
|
||||
}));
|
||||
timesBefore = timesAfter;
|
||||
return Math.floor(timeDeltas.map(times => 1 - times.idle / (times.user + times.sys + times.idle)).reduce((l1, l2) => l1 + l2) / timeDeltas.length*100);
|
||||
}
|
||||
|
||||
app.get("/", ({res}) => {
|
||||
return res.status(200).json({
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
cpu: {
|
||||
avg: getAverageUsage(),
|
||||
cores: os.cpus().length,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// v1 routes
|
||||
const app_v1 = express.Router();
|
||||
app.use("/v1", app_v1);
|
||||
|
||||
// Send Sessions
|
||||
app_v1.get("/", ({res}) => res.json(Object.keys(bdsCore.globalPlatfroms.internalSessions).map(key => {
|
||||
return {
|
||||
id: bdsCore.globalPlatfroms.internalSessions[key].id,
|
||||
platform: bdsCore.globalPlatfroms.internalSessions[key].platform,
|
||||
serverStarted: bdsCore.globalPlatfroms.internalSessions[key].serverStarted,
|
||||
portListen: bdsCore.globalPlatfroms.internalSessions[key].portListening,
|
||||
playerActions: bdsCore.globalPlatfroms.internalSessions[key].playerActions,
|
||||
};
|
||||
})));
|
||||
|
||||
// Install server
|
||||
app_v1.put("/server", async (req, res, next) => {
|
||||
try {
|
||||
if (!req.body||Object.keys(req.body).length === 0) return res.status(400).json({
|
||||
error: "Body is empty"
|
||||
});
|
||||
const action = req.body.action as "install"|"start";
|
||||
const platform = req.body.platform as bdsCore.platformPathManeger.bdsPlatform;
|
||||
if (action === "install" && req.body.version === undefined) req.body.version = "latest";
|
||||
|
||||
if (platform === "bedrock") {
|
||||
if (action === "install") return res.json(await bdsCore.Bedrock.installServer(req.body.version, req.body.platformOptions));
|
||||
else if (action === "start") {
|
||||
const platform = await bdsCore.Bedrock.startServer(req.body.platformOptions);
|
||||
return res.json({
|
||||
id: platform.id
|
||||
});
|
||||
}
|
||||
} else if (platform === "pocketmine") {
|
||||
if (action === "install") return res.json(await bdsCore.PocketmineMP.installServer(req.body.version, req.body.platformOptions));
|
||||
else if (action === "start") {
|
||||
const platform = await bdsCore.PocketmineMP.startServer(req.body.platformOptions);
|
||||
return res.json({
|
||||
id: platform.id
|
||||
});
|
||||
}
|
||||
} else if (platform === "java") {
|
||||
if (action === "install") return res.json(await bdsCore.Java.installServer(req.body.version, req.body.platformOptions));
|
||||
else if (action === "start") {
|
||||
const platform = await bdsCore.Java.startServer(req.body.javaOptions);
|
||||
return res.json({
|
||||
id: platform.id
|
||||
});
|
||||
}
|
||||
} else if (platform === "paper") {
|
||||
if (action === "install") return res.json(await bdsCore.PaperMC.installServer(req.body.version, req.body.platformOptions));
|
||||
else if (action === "start") {
|
||||
const platform = await bdsCore.PaperMC.startServer(req.body.javaOptions);
|
||||
return res.json({
|
||||
id: platform.id
|
||||
});
|
||||
}
|
||||
} else if (platform === "spigot") {
|
||||
if (action === "install") return res.json(await bdsCore.Spigot.installServer(req.body.version, req.body.platformOptions));
|
||||
else if (action === "start") {
|
||||
const platform = await bdsCore.Spigot.startServer(req.body.javaOptions);
|
||||
return res.json({
|
||||
id: platform.id
|
||||
});
|
||||
}
|
||||
} else if (platform === "powernukkit") {
|
||||
if (action === "install") return res.json(await bdsCore.Powernukkit.installServer(req.body.version, req.body.platformOptions));
|
||||
else if (action === "start") {
|
||||
const platform = await bdsCore.Powernukkit.startServer(req.body.javaOptions);
|
||||
return res.json({
|
||||
id: platform.id
|
||||
});
|
||||
}
|
||||
}} catch(err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return res.status(400).json({
|
||||
error: "Invalid action"
|
||||
});
|
||||
});
|
||||
|
||||
app_v1.get("/log/:id", (req, res) => {
|
||||
const session = bdsCore.globalPlatfroms.internalSessions[req.params.id];
|
||||
if (!session) return res.status(400).json({error: "Session ID not exists!"});
|
||||
res.status(200);
|
||||
if (session.serverCommand.options?.logPath?.stdout) fs.createReadStream(session.serverCommand.options.logPath.stdout, {autoClose: false, emitClose: false}).on("data", data => res.write(data));
|
||||
session.events.on("log", data => res.write(Buffer.from(data+"\n")));
|
||||
session.events.once("exit", () => {if (res.closed) res.end()});
|
||||
return res;
|
||||
});
|
||||
|
||||
app_v1.put("/command/:id", (req, res) => {
|
||||
const session = bdsCore.globalPlatfroms.internalSessions[req.params.id];
|
||||
if (!session) return res.status(400).json({error: "Session ID not exists!"});
|
||||
session.runCommand(req.body?.command);
|
||||
return res;
|
||||
});
|
||||
|
||||
app_v1.get("/stop/:id", (req, res, next) => {
|
||||
const session = bdsCore.globalPlatfroms.internalSessions[req.params.id];
|
||||
if (!session) return res.status(400).json({error: "Session ID not exists!"});
|
||||
return session.stopServer().then(exitCode => res.status(200).json({exitCode})).catch(err => next(err));
|
||||
});
|
||||
|
||||
// v2
|
||||
const app_v2 = express.Router();
|
||||
app.use("/v2", app_v2);
|
||||
|
||||
// list sessions
|
||||
app_v2.get("/", async (req, res) => {});
|
||||
|
||||
// Errors pages
|
||||
app.all("*", (req, res) => res.status(404).json({
|
||||
error: "Page not found",
|
||||
path: req.path
|
||||
}));
|
||||
app.use((error, _1, res, _3) => {
|
||||
return res.status(500).json({
|
||||
internalError: String(error).replace(/Error:\s+/, ""),
|
||||
});
|
||||
});
|
||||
51
src/index.ts
51
src/index.ts
@@ -1,14 +1,37 @@
|
||||
export * as platformPathManeger from "./platformPathManeger"
|
||||
export * as globalPlatfroms from "./globalPlatfroms";
|
||||
export * as pluginManeger from "./plugin/plugin";
|
||||
export * as export_import from "./export_import";
|
||||
export * as process_load from "./lib/processLoad";
|
||||
export * as PocketmineMP from "./pocketmine";
|
||||
export * as pluginHooks from "./plugin/hook";
|
||||
export * as Powernukkit from "./pwnuukit";
|
||||
export * as httpRequest from "./lib/httpRequest";
|
||||
export * as PaperMC from "./paper";
|
||||
export * as Bedrock from "./bedrock";
|
||||
export * as Spigot from "./spigot";
|
||||
export * as proxy from "./lib/proxy";
|
||||
export * as Java from "./java";
|
||||
// Utils
|
||||
import * as httpRequest from "./lib/httpRequest";
|
||||
import * as platformPathManeger from "./platformPathManeger"
|
||||
import * as globalPlatfroms from "./globalPlatfroms";
|
||||
import * as pluginManeger from "./plugin/plugin";
|
||||
import * as export_import from "./export_import";
|
||||
import * as process_load from "./lib/processLoad";
|
||||
import * as pluginHooks from "./plugin/hook";
|
||||
import * as proxy from "./lib/proxy";
|
||||
|
||||
// Platforms
|
||||
import * as Bedrock from "./bedrock";
|
||||
import * as Java from "./java";
|
||||
import * as PocketmineMP from "./pocketmine";
|
||||
import * as Spigot from "./spigot";
|
||||
import * as Powernukkit from "./pwnuukit";
|
||||
import * as PaperMC from "./paper";
|
||||
|
||||
export {platformPathManeger, globalPlatfroms, pluginManeger, export_import, process_load, PocketmineMP, pluginHooks, Powernukkit, httpRequest, PaperMC, Bedrock, Spigot, proxy, Java};
|
||||
export default {
|
||||
Bedrock,
|
||||
Java,
|
||||
PocketmineMP,
|
||||
Powernukkit,
|
||||
PaperMC,
|
||||
Spigot,
|
||||
utils: {
|
||||
platformPathManeger,
|
||||
globalPlatfroms,
|
||||
pluginManeger,
|
||||
pluginHooks,
|
||||
httpRequest,
|
||||
export_import,
|
||||
process_load,
|
||||
proxy
|
||||
}
|
||||
};
|
||||
@@ -1,13 +1,69 @@
|
||||
import { tmpdir } from "node:os";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { Method } from "got";
|
||||
import tar from "tar";
|
||||
import AdmZip from "adm-zip";
|
||||
|
||||
let got: (typeof import("got"))["default"];
|
||||
const gotCjs = async () => got||(await (eval('import("got")') as Promise<typeof import("got")>)).default;
|
||||
const gotCjs = async () => got||(await (eval('import("got")') as Promise<typeof import("got")>)).default.extend({enableUnixSockets: true});
|
||||
gotCjs().then(res => got = res);
|
||||
|
||||
export type requestOptions = {
|
||||
path?: string,
|
||||
url?: string,
|
||||
socket?: {path: string, protocoll?: "http"|"https"},
|
||||
method?: Method,
|
||||
headers?: {[headerName: string]: string[]|string},
|
||||
body?: any,
|
||||
};
|
||||
|
||||
export async function pipeFetch(options: requestOptions & {stream: fs.WriteStream}) {
|
||||
let urlRequest: string;
|
||||
if (options.url) urlRequest = options.url+options.path;
|
||||
else if (options.socket) urlRequest = `${options.socket.protocoll||"http"}://unix:${options.socket.path}:${options.path||"/"}`;
|
||||
else throw new Error("Enter a url or an (IPC/Unix) socket");
|
||||
const gotStream = (await gotCjs()).stream(urlRequest, {
|
||||
isStream: true,
|
||||
headers: (!options.headers)?{}:options.headers,
|
||||
method: options.method||"GET",
|
||||
body: options.body,
|
||||
});
|
||||
await new Promise<void>((done, reject) => {
|
||||
options.stream.on("error", reject);
|
||||
gotStream.on("error", reject);
|
||||
gotStream.once("end", () => options.stream.once("finish", done));
|
||||
});
|
||||
}
|
||||
|
||||
export async function bufferFetch(options: requestOptions) {
|
||||
let urlRequest: string;
|
||||
if (options.url) urlRequest = options.url+options.path;
|
||||
else if (options.socket) urlRequest = `${options.socket.protocoll||"http"}://unix:${options.socket.path}:${options.path||"/"}`;
|
||||
else throw new Error("Enter a url or an (IPC/Unix) socket");
|
||||
return gotCjs().then(request => request(urlRequest, {
|
||||
headers: (!options.headers)?{}:options.headers,
|
||||
method: options.method||"GET",
|
||||
body: options.body,
|
||||
responseType: "buffer",
|
||||
})).then(res => ({headers: res.headers, data: Buffer.from(res.body), response: res}));
|
||||
}
|
||||
|
||||
export async function getBuffer(url: string, options?: {method?: string, body?: any, headers?: {[key: string]: string}}): Promise<Buffer> {
|
||||
const urlPar = new URL(url);
|
||||
return bufferFetch({
|
||||
path: urlPar.pathname,
|
||||
url: urlPar.protocol+"//"+urlPar.host,
|
||||
headers: options?.headers,
|
||||
body: options?.body,
|
||||
method: options.method as any
|
||||
}).then(({data}) => data);
|
||||
}
|
||||
|
||||
export async function getJSON<JSONReturn = any>(url: string|requestOptions, options?: requestOptions): Promise<JSONReturn> {
|
||||
return bufferFetch(typeof url === "string"?{...(options||{}), url}:url).then(res => JSON.parse(res.data.toString("utf8")) as JSONReturn);
|
||||
}
|
||||
|
||||
export async function saveFile(url: string, options?: {filePath?: string, headers?: {[key: string]: string|number}}) {
|
||||
const Headers = {};
|
||||
let fileSave = path.join(tmpdir(), Date.now()+"_raw_bdscore_"+path.basename(url));
|
||||
@@ -27,21 +83,6 @@ export async function saveFile(url: string, options?: {filePath?: string, header
|
||||
return fileSave;
|
||||
}
|
||||
|
||||
export async function getBuffer(url: string, options?: {method?: string,body?: any, headers?: {[key: string]: string}}): Promise<Buffer> {
|
||||
const Headers = {};
|
||||
let Body: any;
|
||||
if (options) {
|
||||
if (options.headers) Object.keys(options.headers).forEach(key => Headers[key] = options.headers[key]);
|
||||
if (options.body) Body = options.body;
|
||||
}
|
||||
return (await gotCjs())(url, {
|
||||
headers: Headers,
|
||||
body: Body,
|
||||
method: (options?.method||"GET").toUpperCase() as any,
|
||||
responseType: "buffer"
|
||||
}).then(({body}) => Buffer.from(body));
|
||||
}
|
||||
|
||||
export async function tarExtract(url: string, options?: {folderPath?: string, headers?: {[key: string]: string|number}}) {
|
||||
let fileSave = path.join(tmpdir(), "_bdscore", Date.now()+"_raw_bdscore");
|
||||
const Headers = {};
|
||||
@@ -92,14 +133,6 @@ export async function extractZip(url: string, folderTarget: string) {
|
||||
return extract(folderTarget);
|
||||
}
|
||||
|
||||
export async function getJSON<JSONReturn = any>(url: string, options?: {method?: string, body?: any, headers?: {[key: string]: string}}): Promise<JSONReturn> {
|
||||
return getBuffer(url, {
|
||||
body: options?.body,
|
||||
headers: options?.headers,
|
||||
method: options?.method
|
||||
}).then(res => JSON.parse(res.toString("utf8")) as JSONReturn);
|
||||
}
|
||||
|
||||
export type testIpv6 = {
|
||||
ip: string,
|
||||
type: "ipv4"|"ipv6",
|
||||
|
||||
@@ -113,8 +113,19 @@ export async function changeDefault(platform: bdsPlatform, id: bdsPlatformOption
|
||||
*/
|
||||
export async function getIds(platform?: bdsPlatform) {
|
||||
if (!platform) {
|
||||
const platformIds: {[platform: string]: string[]} = {};
|
||||
await Promise.all((await fs.readdir(bdsRoot)).map(platforms => fs.readdir(path.join(bdsRoot, platforms)).then(data => platformIds[platforms] = data.filter(folder => folder !== "default"))));
|
||||
const platformIds: {[platform: string]: {id: string, realID?: string}[]} = {};
|
||||
if (!await exists(bdsRoot)) return platformIds;
|
||||
const Platforms = await fs.readdir(bdsRoot);
|
||||
if (Platforms.filter(folder => !platformArray.includes(folder as bdsPlatform)).length > 0) throw new Error("Old or invalid Platform path.");
|
||||
for (const Platform of Platforms) {
|
||||
for (const id of await fs.readdir(path.join(bdsRoot, Platform))) {
|
||||
if (!platformIds[Platform]) platformIds[Platform] = []
|
||||
const idPlatform = path.join(bdsRoot, Platform, id);
|
||||
const realPath = await fs.realpath(idPlatform);
|
||||
if (idPlatform !== realPath) platformIds[Platform].push({id, realID: path.basename(realPath)});
|
||||
else platformIds[Platform].push({id});
|
||||
}
|
||||
}
|
||||
return platformIds;
|
||||
}
|
||||
if (!platformArray.includes(platform)) throw new Error("Invalid platform");
|
||||
|
||||
Reference in New Issue
Block a user