Core apis #528

Merged
Sirherobrine23 merged 3 commits from coreApis into main 2023-06-12 01:39:52 +00:00
23 changed files with 1070 additions and 1182 deletions

View File

@ -1,14 +0,0 @@
# npm
/*.tgz
# Node
node_modules/
# Typescript
**/*.js
**/*.d.ts
**/tsconfig.tsbuildinfo
# PHP and Spigot Pre builds
phpOutput/
*.jar

View File

@ -38,7 +38,7 @@ jobs:
# Build Core # Build Core
- name: Core Build - name: Core Build
if: matrix.package != '@the-bds-maneger/core' if: matrix.package != '@the-bds-maneger/core'
run: npm run -w "@the-bds-maneger/core" build run: npm run -w "@the-bds-maneger/core" prepack
- name: Build "${{ matrix.package }}" - name: Build "${{ matrix.package }}"
run: npm run --if-present -w "${{ matrix.package }}" prepack run: npm run --if-present -w "${{ matrix.package }}" prepack

View File

@ -5,91 +5,10 @@ import yargs from "yargs";
// Init yargs // Init yargs
yargs(process.argv.slice(2)).version(false).help(true).strictCommands().demandCommand().alias("h", "help") yargs(process.argv.slice(2)).version(false).help(true).strictCommands().demandCommand().alias("h", "help")
.command("install", "Install server", async yargs => { .command(["install", "i", "update"], "Install/update server platform", yargs => yargs, async options => {
const options = yargs.option("platform", { console.log(bdsCore);
type: "string",
string: true,
alias: "p",
choices: ["bedrock", "java"],
default: "bedrock",
description: "Platform to install"
})
.option("altserver", {
type: "string",
string: true,
alias: "a",
choices: ["spigot", "paper", "purpur", "pocketmine", "powernukkit", "nukkit", "cloudbust"],
})
.option("id", {
alias: "i",
type: "string",
describe: "ID to update server"
})
.option("version", {
type: "string",
alias: "V",
describe: "Server version",
default: "latest"
})
.option("beta", {
type: "boolean",
boolean: true,
default: false,
describe: "allow install beta or snapshort versions"
})
.parseSync();
const serverPath = await bdsCore.serverManeger.serverManeger(options.platform === "java" ? "java" : "bedrock", {
...(options.id ? {newID: false, ID: options.id} : {newID: true}),
});
const installData = await (options.platform === "java" ? bdsCore.Java.installServer : bdsCore.Bedrock.installServer)(Object.assign({}, serverPath, {
version: options.version,
altServer: options.altserver as never,
allowBeta: Boolean(options.beta)
}));
console.log("ID: %O, Server Version: %O, Server Date: %O", installData.id, installData.version, installData.date);
})
.command("list", "list all versions", yargs => {
const { platform, altserver } = yargs.option("platform", {
type: "string",
string: true,
alias: "p",
choices: ["bedrock", "java"],
default: "bedrock",
description: "Platform to install"
}).option("altserver", {
type: "string",
string: true,
alias: "a",
choices: ["spigot", "paper", "purpur", "pocketmine", "powernukkit", "nukkit", "cloudbust"],
}).parseSync();
return (platform === "java" ? bdsCore.Java.listVersions : bdsCore.Bedrock.listVersions)(altserver as never).then(data => console.log(JSON.stringify(data, null, 2)));
})
.command("run", "Start server", async yargs => {
const option = yargs.option("id", {
type: "string",
string: true,
alias: "d",
demandOption: true,
}).parseSync();
const idInfo = (await bdsCore.listIDs()).find(local => local.id === option.id);
if (!idInfo) throw new Error("Invalid ID");
const sserverPaths = await bdsCore.serverManeger.serverManeger(option.platform === "java" ? "java" : "bedrock", {ID: option.id, newID: false});
const session = await (idInfo.platform === "java" ? bdsCore.Java.startServer : bdsCore.Bedrock.startServer)(sserverPaths);
process.on("error", console.log);
session.once("backup", filePath => console.log("Backup file path: %O", filePath));
process.stdin.pipe(session.stdin);
session.stdout.pipe(process.stdout);
session.stderr.pipe(process.stderr);
for (const ss of ([
"SIGCONT",
"SIGINT",
"SIGTERM",
] as NodeJS.Signals[])) process.on(ss, () => session.stopServer());
return session;
}) })
.command(["start", "run", "$0"], "start server", yargs => yargs, async options => {})
.parseAsync().catch(err => { .parseAsync().catch(err => {
console.log(err); console.log(err);
process.exit(1); process.exit(1);

View File

@ -1,37 +1,3 @@
# Bds Maneger Core # Bds Maneger Core
Basic core to install, update and manage several minecraft servers automatically, depending on a few dependencies, the basic being **Nodejs**. Um simples nucleo com exposição de APIs para integração com os Servidores de Minecraft para gerenciamento no Nodejs.
## Servers supports and TODO
**Bedrock Mojang**:
- [x] Install/Update.
- [ ] Hot backup.
- [x] Start.
- [x] Port Listened.
- [ ] Player connect/disconnect/spawn.
- [ ] Player kick/ban.
**Pocketmine PMMP**:
- [x] Install/Update.
- [ ] Hot backup.
- [x] Start.
- [ ] Port listened.
- [ ] Player connect/disconnect.
- [ ] Player kick/ban.
**Powernukkit** and **Cloudbust**:
- [x] Install/Update.
- 🚫 Hot backup.
- [x] Start.
- [ ] Port listened.
- [ ] Player connect/disconnect.
- [ ] Player kick/ban.
**Java Mojang**, **Purpur**, **Paper** and **Spigot**:
- [x] Install/Update.
- 🚫 Hot Backup.
- [x] Start.
- [ ] Port listened.
- [ ] Player connect/disconect action.
- [ ] Player kick/ban.

View File

@ -1,7 +1,7 @@
{ {
"name": "@the-bds-maneger/core", "name": "@the-bds-maneger/core",
"version": "6.0.4", "version": "6.0.4",
"description": "", "description": "Core APIs to maneger Minecraft servers platforms",
"main": "src/index.js", "main": "src/index.js",
"types": "src/index.d.ts", "types": "src/index.d.ts",
"type": "module", "type": "module",
@ -11,24 +11,22 @@
"access": "public" "access": "public"
}, },
"scripts": { "scripts": {
"prepack": "tsc --build --clean && tsc", "prepack": "tsc --build --clean && tsc --build",
"postpack": "tsc --build --clean", "postpack": "tsc --build --clean"
"build": "tsc --build --clean && tsc"
}, },
"dependencies": { "dependencies": {
"@sirherobrine23/cloud": "^3.6.11", "@sirherobrine23/cloud": "^3.6.11",
"@sirherobrine23/extends": "^3.6.11", "@sirherobrine23/extends": "^3.6.11",
"@sirherobrine23/http": "^3.6.11", "@sirherobrine23/http": "^3.6.11",
"adm-zip": "0.5.10",
"sanitize-filename": "^1.6.3", "sanitize-filename": "^1.6.3",
"semver": "^7.5.1", "semver": "^7.5.1",
"tar": "^6.1.15", "tar": "^6.1.15",
"unzip-stream": "^0.3.1", "xml-js": "^1.6.11"
"unzipper": "^0.10.14"
}, },
"devDependencies": { "devDependencies": {
"@types/adm-zip": "0.5.0",
"@types/semver": "^7.5.0", "@types/semver": "^7.5.0",
"@types/tar": "^6.1.5", "@types/tar": "^6.1.5"
"@types/unzip-stream": "^0.3.1",
"@types/unzipper": "^0.10.6"
} }
} }

View File

@ -1,5 +0,0 @@
import { oracleBucket } from "@sirherobrine23/cloud";
/**
* Bucket readonly
*/
export const oracleStorage = oracleBucket.oracleBucketPreAuth("sa-saopaulo-1", "grwodtg32n4d", "bdsFiles", "0IKM-5KFpAF8PuWoVe86QFsF4sipU2rXfojpaOMEdf4QgFQLcLlDWgMSPHWmjf5W");

View File

@ -0,0 +1,5 @@
import { oracleBucket } from "@sirherobrine23/cloud";
/**
* Bucket readonly
*/
export const bdsFilesBucket = oracleBucket.oracleBucketPreAuth("sa-saopaulo-1", "grwodtg32n4d", "bdsFiles", "0IKM-5KFpAF8PuWoVe86QFsF4sipU2rXfojpaOMEdf4QgFQLcLlDWgMSPHWmjf5W");

View File

@ -0,0 +1,2 @@
export * as listVersion from "./listVersion.js";
export * from "./main.js";

View File

@ -0,0 +1,184 @@
import { http, Github } from "@sirherobrine23/http";
import { versionsStorages } from "../../serverRun.js";
import util from "node:util";
import xml from "xml-js";
export interface mojangInfo {
date: Date,
release?: "oficial" | "preview",
url: {
[platform in NodeJS.Platform]?: {
[arch in NodeJS.Architecture]?: {
[ext in "tgz" | "zip"]?: string;
}
}
}
}
export const mojangCache = new versionsStorages<mojangInfo>();
export async function listMojang() {
const versions = await http.jsonRequestBody<({version: string} & mojangInfo)[]>("https://raw.githubusercontent.com/Sirherobrine23/BedrockFetch/main/versions/all.json");
versions.filter(ver => !(mojangCache.has(ver.version))).forEach(rel => mojangCache.set(rel.version, {
date: new Date(rel.date),
release: rel.release,
url: rel.url
}));
}
type powerNukkitRelease = {
version: string;
minecraftVersion: string;
releaseTime: number;
commitId: string;
snapshotBuild?: number;
artefacts: string[];
}
export interface powernukkitDownload {
version: string;
releaseDate: Date;
releaseType: "snapshot"|"oficial";
url: string;
}
export const powernukkitCache = new versionsStorages<powernukkitDownload>();
export async function listPowernukkitProject() {
const releases = await http.jsonRequestBody<{[k in "releases"|"snapshots"]: powerNukkitRelease[]}>("https://raw.githubusercontent.com/PowerNukkit/powernukkit-version-aggregator/master/powernukkit-versions.json");
const releasesData = (Object.keys(releases) as (keyof typeof releases)[]).map(releaseType => releases[releaseType].map(data => ({...data, releaseType}))).flat(1).sort((b, a) => Math.min(1, Math.max(-1, a.releaseTime - b.releaseTime)));
for (const data of releasesData) {
if (powernukkitCache.has(data.minecraftVersion)) continue;
const releateDate = new Date(data.releaseTime);
const getArtefactExtension = (artefactId: string) => (artefactId.includes("REDUCED_JAR")) ? ".jar" : (artefactId.includes("REDUCED_SOURCES_JAR")) ? "-sources.jar" : (artefactId.includes("SHADED_JAR")) ? "-shaded.jar" : (artefactId.includes("SHADED_SOURCES_JAR")) ? "-shaded-sources.jar" : (artefactId.includes("JAVADOC_JAR")) ? "-javadoc.jar" : ".unknown";
function buildArtefactUrl(data: any, artefactId?: string) {
const buildReleaseArtefactUrl = (data: any, artefactId?: string) => !data.artefacts.includes(artefactId) ? null : util.format("https://search.maven.org/remotecontent?filepath=org/powernukkit/powernukkit/%s/powernukkit-%s%s", data.version, data.version, getArtefactExtension(artefactId));
const buildSnapshotArtefactUrl = (data: any, artefactId?: string) => !data.artefacts.includes(artefactId) ? null : util.format("https://oss.sonatype.org/content/repositories/snapshots/org/powernukkit/powernukkit/%s-SNAPSHOT/powernukkit-%s-%s%s", data.version.substring(0, data.version.indexOf("-SNAPSHOT")), data.version.substring(0, data.version.indexOf("-SNAPSHOT")), releateDate.getUTCFullYear().toString().padStart(4, "0") + (releateDate.getUTCMonth() + 1).toString().padStart(2, "0") + releateDate.getUTCDate().toString().padStart(2, "0") + "." + releateDate.getUTCHours().toString().padStart(2, "0") + releateDate.getUTCMinutes().toString().padStart(2, "0") + releateDate.getUTCSeconds().toString().padStart(2, "0") + "-" + data.snapshotBuild, getArtefactExtension(artefactId));
if (artefactId == "GIT_SOURCE") {
if (data.commitId) return util.format("https://github.com/PowerNukkit/PowerNukkit/tree/%s", data.commitId);
else if (data.snapshotBuild && data.artefacts.includes("SHADED_SOURCES_JAR")) return buildSnapshotArtefactUrl(data, "SHADED_SOURCES_JAR");
else if (data.snapshotBuild && data.artefacts.includes("REDUCED_SOURCES_JAR")) return buildSnapshotArtefactUrl(data, "REDUCED_SOURCES_JAR");
else if (data.artefacts.includes("SHADED_SOURCES_JAR")) return buildReleaseArtefactUrl(data, "SHADED_SOURCES_JAR");
else if (data.artefacts.includes("REDUCED_SOURCES_JAR")) return buildReleaseArtefactUrl(data, "REDUCED_SOURCES_JAR");
} else if (data.snapshotBuild) return buildSnapshotArtefactUrl(data, artefactId);
return buildReleaseArtefactUrl(data, artefactId);
}
const artefacts = data.artefacts.reduce<{[key: string]: string}>((acc, artefactId) => {acc[artefactId] = buildArtefactUrl(data, artefactId); return acc;}, {});
if (!(artefacts.SHADED_JAR || artefacts.REDUCED_JAR)) continue;
powernukkitCache.set(data.minecraftVersion, {
version: data.minecraftVersion,
releaseDate: releateDate,
releaseType: data.releaseType === "releases" ? "oficial" : "snapshot",
url: artefacts.SHADED_JAR || artefacts.REDUCED_JAR,
});
}
}
export interface cloudburstDownload {
releaseDate: Date;
url: string;
}
export const nukkitCache = new versionsStorages<cloudburstDownload>();
export const cloudburstCache = new versionsStorages<cloudburstDownload>();
export async function listCloudburstProject() {
const Projects = [ "Nukkit", "Server" ] as const;
for (const Project of Projects) {
const { body: { jobs } } = await http.jsonRequest<{jobs: {name: string, _class: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${Project}/api/json`);
const buildFiles = await Promise.all(jobs.filter(b => b._class === "org.jenkinsci.plugins.workflow.job.WorkflowJob").map(b => b.name).map(async branch => {
const { body: { builds } } = await http.jsonRequest<{builds: {_class: string, number: number, url: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${Project}/job/${branch}/api/json`);
return Promise.all(builds.map(async build => {
const { body: { artifacts, result, timestamp, actions } } = await http.jsonRequest<{result: "SUCCESS", actions: {_class: string, [k: string]: any}[], timestamp: number, artifacts: {displayPath: string, fileName: string, relativePath: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${Project}/job/${branch}/${build.number}/api/json`);
if (result !== "SUCCESS") return [];
const branchBuild = actions.find(r => typeof r["buildsByBranchName"] === "object");
if (!branch) return [];
const commitID = ((branchBuild?.buildsByBranchName[Object.keys(branchBuild.buildsByBranchName).at(0)]?.marked?.SHA1 || branchBuild.buildsByBranchName[Object.keys(branchBuild.buildsByBranchName).at(0)]?.revision?.SHA1) as string|undefined)
let mcpeVersion: string;
if (Project === "Server") {
const json = xml.xml2js((await http.bufferRequestBody(`https://raw.githubusercontent.com/CloudburstMC/Server/${commitID}/pom.xml`)).toString("utf8"), {compact: true});
const info = json["project"].dependencies.dependency.find(dep => dep.groupId._text === "com.nukkitx");
mcpeVersion = info.version._text;
} else {
try {
// https://raw.githubusercontent.com/CloudburstMC/Nukkit/master/src/main/java/cn/nukkit/network/protocol/ProtocolInfo.java
const lines = (await http.bufferRequestBody(`https://raw.githubusercontent.com/CloudburstMC/Nukkit/${commitID}/src/main/java/cn/nukkit/network/protocol/ProtocolInfo.java`)).toString("utf8").split("\n");
const versions = lines.filter(l => l.trim().startsWith("String") && l.toLowerCase().includes("version")).reduce<{[k: string]: string}>((acc, line) => {
line = line.trim().slice(6).trim();
const i = line.indexOf("=");
const def = line.slice(0, i).trim();
let version = line.slice(i+1).trim().slice(1, -1);
if (version.endsWith("\"")) version = version.slice(0, -1).trim();
if (version.startsWith("v")) version = version.slice(1);
if (!(version.includes("+"))) acc[def] = version;
return acc;
}, {});
mcpeVersion = versions.MINECRAFT_VERSION || versions.MINECRAFT_VERSION_NETWORK;
} catch {
return [];
}
}
if (!mcpeVersion) return [];
return artifacts.filter(f => f.relativePath.endsWith(".jar")).map(target => ({
buildNumber: build.number,
branch,
mcpeVersion,
releaseDate: new Date(timestamp),
url: `https://ci.opencollab.dev/job/NukkitX/job/${Project}/job/${branch}/${build.number}/artifact/${target.relativePath}`,
}));
}));
})).then(r => r.flat(2));
for (const build of buildFiles.sort((b, a) => a.releaseDate.getTime() - b.releaseDate.getTime())) {
if (Project === "Server") {
if (cloudburstCache.has(build.mcpeVersion)) continue;
cloudburstCache.set(build.mcpeVersion, {
releaseDate: build.releaseDate,
url: build.url
});
} else {
if (nukkitCache.has(build.mcpeVersion)) continue;
nukkitCache.set(build.mcpeVersion, {
releaseDate: build.releaseDate,
url: build.url
});
}
}
}
}
export interface pocketmineDownload {
releateDate: Date;
releaseType: "preview" | "oficial";
url: string;
}
export const pocketmineCache = new versionsStorages<pocketmineDownload>();
const pocketmineGithub = await Github.repositoryManeger("pmmp", "PocketMine-MP");
export async function listPocketmineProject() {
const pocketmineReleases = (await pocketmineGithub.release.getRelease()).filter(rel => (rel.assets.find(assert => assert.name.endsWith(".phar")) ?? {}).browser_download_url);
for (const data of pocketmineReleases) {
if (pocketmineCache.has(data.tag_name)) continue;
const assest = data.assets.find(assert => assert.name.endsWith(".phar"));
pocketmineCache.set(data.tag_name, {
releaseType: data.prerelease ? "preview" : "oficial",
releateDate: new Date(assest.created_at),
url: assest.browser_download_url
});
};
}
export function getCacheVersions() {
return {
mojang: Array.from(mojangCache.keys()).reduce<{[k: string]: mojangInfo}>((acc, key) => {acc[key] = mojangCache.get(key); return acc;}, {}),
pocketmine: Array.from(pocketmineCache.keys()).reduce<{[k: string]: pocketmineDownload}>((acc, key) => {acc[key] = pocketmineCache.get(key); return acc;}, {}),
powernukkit: Array.from(powernukkitCache.keys()).reduce<{[k: string]: powernukkitDownload}>((acc, key) => {acc[key] = powernukkitCache.get(key); return acc;}, {}),
nukkit: Array.from(nukkitCache.keys()).reduce<{[k: string]: cloudburstDownload}>((acc, key) => {acc[key] = nukkitCache.get(key); return acc;}, {}),
cloudburst: Array.from(cloudburstCache.keys()).reduce<{[k: string]: cloudburstDownload}>((acc, key) => {acc[key] = cloudburstCache.get(key); return acc;}, {}),
};
}
export async function syncCaches() {
await Promise.all([
listMojang(),
listCloudburstProject(),
listPocketmineProject(),
listPowernukkitProject()
]);
}

View File

@ -0,0 +1,288 @@
import { extendsFS } from "@sirherobrine23/extends";
import { http } from "@sirherobrine23/http";
import AdmZip from "adm-zip";
import child_process from "node:child_process";
import { createWriteStream } from "node:fs";
import fs from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import readline from "node:readline";
import { finished } from "node:stream/promises";
import tar from "tar";
import { bdsFilesBucket } from "../../internalClouds.js";
import { customEvent, defineEvents } from "../../serverRun.js";
import * as bedrockVersions from "./listVersion.js";
export type platforms = "mojang" | "pocketmine" | "powernukkit" | "nukkit" | "cloudburst";
export interface bedrockPorts {
port: number;
localListen?: string;
};
export interface playerInfo {
connected: boolean;
banned: boolean;
xuid?: string;
historic: {
action: string;
actionDate: Date;
}[];
};
class playerListen extends Map<string, playerInfo> {
constructor(origem?: Record<string, playerInfo>) {
super(Object.keys(origem || {}).map(key => ([key, origem[key]])));
}
toJSON() {
return Array.from(this.keys()).reduce<{ [playerName: string]: playerInfo }>((acc, player) => {
acc[player] = this.get(player);
return acc;
}, {});
}
init(playerName: string): this {
if (!(this.has(playerName))) {
super.set(playerName, {
banned: false,
connected: false,
historic: [],
});
}
return this;
}
updateState(playerName: string, state: playerInfo["historic"][number]["action"]) {
const actionDate = new Date();
const playerData = this.init(playerName).get(playerName);
if (state === "disconnected") playerData.connected = false; else playerData.connected = true;
playerData.historic.push({ action: state, actionDate });
super.set(playerName, playerData);
}
}
/**
* Return boolean if Class input is Bedrock class server
* @param event - Bedrock class
* @returns
*/
export function isBedrock(event: Bedrock<any>): event is Bedrock<any> {
return event instanceof Bedrock;
}
export type bedrockEvents = defineEvents<{
logLine(lineString: string): void;
portListen(info: bedrockPorts): void;
installedVersion(serverVersion: string): void;
}>;
export class Bedrock<P extends platforms> extends customEvent<bedrockEvents> {
readonly serverFolder: string;
readonly rootServer: string;
readonly platform: P;
constructor(rootServer: string, platform: P) {
super();
if ((!(["mojang", "pocketmine", "powernukkit", "nukkit", "cloudburst"]).includes(platform))) throw new Error("Invalid platform");
this.platform = platform;
this.rootServer = rootServer;
this.serverFolder = path.join(rootServer, "server");
Object.defineProperty(this, "rootServer", { writable: false });
Object.defineProperty(this, "serverFolder", { writable: false });
Object.defineProperty(this, "platform", { writable: false });
}
async installServer(version: string | number) {
const { platform } = this;
if (!(await extendsFS.exists(this.serverFolder))) await fs.mkdir(this.serverFolder, { recursive: true });
if (platform === "mojang") {
const release = bedrockVersions.mojangCache.get(version);
if (!release) throw new Error("Not valid Release");
const serverURL = release.url[process.platform]?.[process.arch]?.tgz;
if (!serverURL) throw new Error("Current platform not support mojang server");
let backupFiles = [{ file: "allowlist.json", data: "" }, { file: "permissions.json", data: "" }, { file: "server.properties", data: "" }];
backupFiles = await Promise.all(backupFiles.map(async file => { file.data = await fs.readFile(path.join(this.serverFolder, file.file), "utf8").catch(() => ""); return file }));
await finished((await http.streamRequest(serverURL)).pipe(tar.extract({ cwd: this.serverFolder, preserveOwner: true, keep: false })));
await Promise.all(backupFiles.filter(file => !!(file.data.trim())).map(async file => fs.writeFile(path.join(this.serverFolder, file.file), file.data)));
this.emit("installedVersion", bedrockVersions.mojangCache.prettyVersion(version));
} else if (platform === "pocketmine") {
const release = bedrockVersions.pocketmineCache.get(version);
if (!release) throw new Error("Not valid Release");
await finished((await http.streamRequest(release.url)).pipe(createWriteStream(path.join(this.serverFolder, "server.phar"))));
let phpFiles = (await bdsFilesBucket.listFiles("php_bin/")).map(file => ({ name: file.name.slice(8).toLowerCase(), data: file })).filter(file => file.name.startsWith(`${process.platform}_${process.arch}`));
if (!phpFiles.length) throw new Error("Cannot get php binary to current platform");
const phpFile = phpFiles.sort((b, a) => a.data.Dates.Modified.getTime() - b.data.Dates.Modified.getTime()).at(0);
const binPath = path.join(this.serverFolder, "bin");
if (await extendsFS.exists(binPath)) await fs.rm(binPath, { recursive: true, force: true });
if (phpFile.name.endsWith(".tar.gz") || phpFile.name.endsWith(".tgz")) await finished((await phpFile.data.getFile()).pipe(tar.extract({ cwd: binPath })));
else if (phpFile.name.endsWith(".zip")) {
const tmpFile = path.join(tmpdir(), Date.now() + "_" + phpFile.name);
await finished((await phpFile.data.getFile()).pipe(createWriteStream(tmpFile)));
await new Promise<void>((done, reject) => (new AdmZip(tmpFile)).extractAllToAsync(binPath, true, true, err => err ? reject(err) : done()));
await fs.rm(tmpFile, { force: true });
}
this.emit("installedVersion", bedrockVersions.pocketmineCache.prettyVersion(version));
} else if (platform === "powernukkit") {
const release = bedrockVersions.powernukkitCache.get(version);
if (!release) throw new Error("Not valid Release");
await finished(await http.streamRequest(release.url), createWriteStream(path.join(this.serverFolder, "server.jar")));
this.emit("installedVersion", bedrockVersions.powernukkitCache.prettyVersion(version));
} else if (platform === "cloudburst" || platform === "nukkit") {
const platformVersions = platform === "cloudburst" ? bedrockVersions.cloudburstCache : bedrockVersions.nukkitCache;
const release = platformVersions.get(version);
if (!release) throw new Error("Not valid Release");
await finished(await http.streamRequest(release.url), createWriteStream(path.join(this.serverFolder, "server.jar")));
this.emit("installedVersion", platformVersions.prettyVersion(version));
}
}
ports: bedrockPorts[] = [];
readonly players = new playerListen();
serverProcess?: child_process.ChildProcess;
async runServer() {
const { platform } = this;
const logLine: (bedrockEvents["logLine"])[] = [];
if (platform === "mojang") {
const fileExec = path.join(this.serverFolder, (await fs.readdir(this.serverFolder)).find(file => file.startsWith("bedrock_server")));
if (process.platform !== "win32") await fs.chmod(fileExec, 0o775);
this.serverProcess = child_process.spawn(fileExec, {
cwd: this.serverFolder,
stdio: ["pipe", "pipe", "pipe"]
});
logLine.push(lineString => {
// [INFO] IPv4 supported, port: 19132
// [2023-03-08 13:01:57 INFO] Listening on IPv4 port: 19132
const ipProtocol = lineString.slice(lineString.indexOf("IPv"), lineString.indexOf("IPv")+4);
if (ipProtocol) {
let port = lineString.slice(lineString.lastIndexOf("port:")+5).trim();
if (port.indexOf(":") !== -1) port = port.slice(0, port.lastIndexOf(":"));
const info: bedrockPorts = {
port: Number(port),
localListen: ipProtocol.toLowerCase() === "ipv4" ? "0.0.0.0" : "[::]",
};
this.ports.push(info);
this.emit("portListen", info);
}
return null;
}, lineString => {
lineString = lineString.replace(/^(.*)?\[.*\]/, "").trim();
if (lineString.startsWith("Player")) {
lineString = lineString.replace("Player", "").trim();
// Spawned, disconnected, connected
let action: string;
if (lineString.startsWith("Spawned")) action = "Spawned";
else if (lineString.startsWith("disconnected")) action = "disconnected";
else if (lineString.startsWith("connected")) action = "connected";
if (!action) return null;
lineString = lineString.replace(action, "").trim();
if (lineString.startsWith(":")) lineString = lineString.slice(1).trim();
let playerName = lineString.substring(0, lineString.indexOf("xuid:")-1).trim();
if (!playerName) return null;
if (playerName.endsWith(",")) playerName = playerName.slice(0, playerName.length - 1);
let xuid: string;
if (lineString.indexOf("xuid:") !== -1) {
xuid = lineString.slice(lineString.indexOf("xuid:")+5).trim();
if (!xuid) xuid = null;
}
if (!(this.players.has(playerName))) this.players.init(playerName);
if (!!xuid) {
if (!(this.players.get(playerName)).xuid) {
const info = this.players.get(playerName);
info.xuid = xuid;
this.players.set(playerName, info);
}
}
this.players.updateState(playerName, action);
}
});
} else if (platform === "pocketmine") {
const phpBin = (await extendsFS.readdirV2(this.serverFolder)).find(file => file.endsWith("php") || file.endsWith("php.exe"));
if (!phpBin) throw new Error("Fist install php binary in server folder");
this.serverProcess = child_process.spawn(phpBin, ["server.phar", "--no-wizard"], {
cwd: this.serverFolder,
stdio: ["pipe", "pipe", "pipe"],
});
} else if (platform === "nukkit" || platform === "powernukkit" || platform === "cloudburst") {
this.serverProcess = child_process.spawn("java", [
"-XX:+UseG1GC",
"-XX:+ParallelRefProcEnabled",
"-XX:MaxGCPauseMillis=200",
"-XX:+UnlockExperimentalVMOptions",
"-XX:+DisableExplicitGC",
"-XX:+AlwaysPreTouch",
"-XX:G1NewSizePercent=30",
"-XX:G1MaxNewSizePercent=40",
"-XX:G1HeapRegionSize=8M",
"-XX:G1ReservePercent=20",
"-XX:G1HeapWastePercent=5",
"-XX:G1MixedGCCountTarget=4",
"-XX:InitiatingHeapOccupancyPercent=15",
"-XX:G1MixedGCLiveThresholdPercent=90",
"-XX:G1RSetUpdatingPauseTimePercent=5",
"-XX:SurvivorRatio=32",
"-XX:+PerfDisableSharedMem",
"-XX:MaxTenuringThreshold=1",
"-Dusing.aikars.flags=https://mcflags.emc.gs",
"-Daikars.new.flags=true",
"-jar", "server.jar",
], {
cwd: this.serverFolder,
stdio: ["pipe", "pipe", "pipe"],
});
}
// Break lines
([
readline.createInterface(this.serverProcess.stdout),
readline.createInterface(this.serverProcess.stderr)
]).map(inter => inter.on("error", err => this.emit("error", err)).on("line", data => {
this.emit("logLine", data);
logLine.forEach(fn => Promise.resolve().then(() => fn(data)).catch(err => this.emit("error", err)));
}));
return this.serverProcess;
}
writeLn(data: string | Buffer) {
this.serverProcess.stdin.write(data);
if (Buffer.isBuffer(data) && (String.fromCharCode(data.at(-1)) !== "\n")) this.serverProcess.stdin.write("\n");
else if (typeof data === "string" && !(data.trim().endsWith("\n"))) this.serverProcess.stdin.write("\n");
return this;
}
async stopServer() {
this.writeLn("stop");
return new Promise<{ code: number, signal: NodeJS.Signals }>((done, reject) => this.serverProcess.once("error", reject).once("exit", (code, signal) => done({ code, signal })));
}
async setPlayerPermission(playername: string, permission: P extends "mojang" ? "operator" | "member" | "visitor" : "admin" | "user") {
if (this.platform === "mojang") {
const permissions: { permission: "operator" | "member" | "visitor", xuid: string }[] = JSON.parse(await fs.readFile(path.join(this.serverFolder, "permissions.json"), "utf8"));
permissions.push({
permission: permission as any,
xuid: playername
});
await fs.writeFile(path.join(this.serverFolder, "permissions.json"), JSON.stringify(permissions));
if (this.serverProcess) this.writeLn("permission reload");
}
}
async allowList(playername: string, options?: { xuid?: string, ignoresPlayerLimit?: boolean }) {
if (this.platform === "mojang") {
const permissions: { ignoresPlayerLimit: boolean, name: string, xuid?: string }[] = JSON.parse(await fs.readFile(path.join(this.serverFolder, "allowlist.json"), "utf8"));
await fs.writeFile(path.join(this.serverFolder, "allowlist.json"), JSON.stringify(permissions));
permissions.push({
name: playername,
ignoresPlayerLimit: options?.ignoresPlayerLimit ?? false,
xuid: options?.xuid
});
if (this.serverProcess) this.writeLn("allowlist reload");
}
}
}

View File

@ -0,0 +1,2 @@
export * as listVersion from "./listVersion.js";
export * from "./main.js";

View File

@ -0,0 +1,175 @@
import { Github, http } from "@sirherobrine23/http";
import stream from "node:stream";
import path from "path";
import semver from "semver";
import { bdsFilesBucket } from "../../internalClouds.js";
import { versionsStorages } from "../../serverRun.js";
interface baseDownload {
URL: string;
releaseDate: Date;
}
interface mojangInfo extends baseDownload {
release: "oficial" | "snapshot" | "beta" | "alpha";
}
async function PromiseSplit<T extends (any[])>(arrayInput: T, fn: (value: T[number]) => any): Promise<Awaited<ReturnType<typeof fn>>[]> {
const backup = ([]).concat(...arrayInput);
let i = arrayInput.length, b = 1; while (i > 2) { b++; i /= 2 };
const arraySplit = Math.max(2, Math.floor(b));
let result: Awaited<ReturnType<typeof fn>>[] = [];
await Promise.all(Array(arraySplit).fill(async function call() {
return Promise.resolve().then(() => backup.length > 0 ? fn(backup.shift()) : null).then(data => {
result.push(data);
if (backup.length > 0) return call();
return null;
});
}).map(a => a()));
return result.flat(1);
}
export const mojangCache = new versionsStorages<mojangInfo>();
export async function listMojang() {
const versions = (await http.jsonRequestBody<{ versions: { id: string, releaseTime: string, url: string, type: "snapshot" | "release" | "old_beta" | "old_alpha" }[] }>("https://launchermeta.mojang.com/mc/game/version_manifest_v2.json")).versions;
await PromiseSplit(versions, async version => {
if (mojangCache.has(version.id)) return;
const { downloads: { server } } = await http.jsonRequestBody<{ downloads: { server?: { url: string } } }>(version.url);
if (!server) return;
mojangCache.set(version.id, {
release: version.type === "release" ? "oficial" : version.type === "snapshot" ? "snapshot" : version.type === "old_beta" ? "beta" : "alpha",
releaseDate: new Date(version.releaseTime),
URL: server.url,
});
});
}
export interface spigotDownload {
getServer(): Promise<stream.Readable>;
craftbukkit?(): Promise<stream.Readable>;
}
export const spigotCache = new versionsStorages<spigotDownload>();
export async function listSpigot() {
const spigotFiles = await bdsFilesBucket.listFiles("SpigotBuild/");
for (const file of spigotFiles.filter(file => file.name.slice(12).startsWith("1.")).sort((b, a) => {
const valid = (c: typeof a) => semver.valid(semver.coerce(c.name.slice(12, -4), { loose: true }), { loose: true });
return semver.compare(valid(a), valid(b));
})) {
const version = file.name.slice(12, -4), fixVersion = semver.valid(semver.coerce(version, { loose: true }), { loose: true });
if (spigotCache.has(fixVersion)) continue;
const craftbukkit = spigotFiles.find(file => file.name.slice(12).startsWith(`craftbukkit-${version}.jar`));
spigotCache.set(fixVersion, {
getServer: file.getFile,
craftbukkit: craftbukkit ? craftbukkit.getFile : undefined,
});
}
}
export const paperCache = new versionsStorages<baseDownload>();
export const velocityCache = new versionsStorages<baseDownload>();
export const foliaCache = new versionsStorages<baseDownload>();
export async function listPaperProject() {
const paperProjects = ["paper", "velocity", "folia"] as const;
await Promise.all(paperProjects.map(async projectName => {
const baseURL = new URL(path.posix.join("/v2/projects", projectName), "https://api.papermc.io");
const projectInfo = await http.jsonRequestBody<{ versions: string[] }>(baseURL);
for (const projectVersion of projectInfo.versions) {
if (projectName === "paper" && paperCache.has(projectVersion)) continue;
else if (projectName === "velocity" && velocityCache.has(projectVersion)) continue;
else if (projectName === "folia" && foliaCache.has(projectVersion)) continue;
else {
const versionBase = new URL(path.posix.join(baseURL.pathname, "versions", projectVersion), baseURL);
const builds = await http.jsonRequestBody<{ builds: number[] }>(versionBase);
for (const build of builds.builds) {
const buildURL = new URL(path.posix.join(versionBase.pathname, "builds", String(build)), versionBase);
const downloadInfo = await http.jsonRequestBody<{ time: string; downloads: { application?: { name: string } } }>(buildURL);
if (downloadInfo.downloads.application) {
const downloadURL = new URL(path.posix.join(buildURL.pathname, "downloads", downloadInfo.downloads.application.name), buildURL);
if (projectName === "paper") paperCache.set(projectVersion, { URL: downloadURL.toString(), releaseDate: new Date(downloadInfo.time) });
else if (projectName === "velocity") velocityCache.set(projectVersion, { URL: downloadURL.toString(), releaseDate: new Date(downloadInfo.time) });
else if (projectName === "folia") foliaCache.set(projectVersion, { URL: downloadURL.toString(), releaseDate: new Date(downloadInfo.time) });
break;
}
}
}
}
}));
}
export const purpurCache = new versionsStorages<baseDownload>();
export async function listPurpurProject() {
const baseURL = new URL("https://api.purpurmc.org/v2/purpur");
const { versions } = await http.jsonRequestBody<{ versions: string[] }>(baseURL);
for (const version of versions) {
if (purpurCache.has(version)) continue;
const infoBase = new URL(path.posix.join(baseURL.pathname, version, "latest"), baseURL);
const relInfo = await http.jsonRequestBody<{ timestamp: number }>(infoBase);
purpurCache.set(version, {
URL: (new URL(path.posix.join(infoBase.pathname, "download"), infoBase)).toString(),
releaseDate: new Date(relInfo.timestamp)
});
}
}
export const glowstoneCache = new versionsStorages<baseDownload>();
export async function listGlowstoneProject() {
const repo = await Github.repositoryManeger("GlowstoneMC", "Glowstone");
const rels = (await repo.release.getRelease()).filter(rel => rel.assets.some(asset => asset.name.endsWith(".jar")));
rels.forEach(rel => rel.assets.forEach(asset => glowstoneCache.has(rel.tag_name) ? null : glowstoneCache.set(rel.tag_name, {
URL: asset.browser_download_url,
releaseDate: new Date(asset.created_at)
})));
}
export const cuberiteCache = new versionsStorages<{ URL: string[] }>({
"win32-x64": {
URL: ["https://download.cuberite.org/windows-x86_64/Cuberite.zip"]
},
"win32-ia32": {
URL: ["https://download.cuberite.org/windows-i386/Cuberite.zip"]
}
});
export async function listCuberite() {
const projects = ["android", "linux-aarch64", "linux-armhf", "linux-i386", "linux-x86_64", "darwin-x86_64"] as const;
await Promise.all(projects.map(async project => {
const { builds = [] } = await http.jsonRequestBody<{ builds: { number: number, _class: string }[] }>(`https://builds.cuberite.org/job/${project}/api/json`);
for (const job of builds) {
const { artifacts = [], result } = await http.jsonRequestBody<{ result: string, artifacts: { relativePath: string, fileName: string }[] }>(`https://builds.cuberite.org/job/${project}/${job.number}/api/json`);
if (result !== "SUCCESS") continue;
let map = artifacts.filter(file => !file.fileName.endsWith(".sha1")).map(file => `https://builds.cuberite.org/job/${project}/${job.number}/artifact/${file.relativePath}`);
if (!map.length) continue;
else if (project === "darwin-x86_64") cuberiteCache.set("darwin-x64", { URL: map });
else if (project === "linux-x86_64") cuberiteCache.set("linux-x64", { URL: map });
else if (project === "linux-aarch64") cuberiteCache.set("linux-arm64", { URL: map });
else if (project === "linux-armhf") cuberiteCache.set("linux-arm", { URL: map });
else if (project === "linux-i386") cuberiteCache.set("linux-ia32", { URL: map });
else if (project === "android") {
const serverIndex = map.findIndex(file => file.toLowerCase().endsWith("server.zip"));
const server = map[serverIndex];
delete map[serverIndex];
map = map.filter(Boolean);
for (const file of map) {
const fileURL = new URL(file);
const plat = path.basename(fileURL.pathname).replace(path.extname(fileURL.pathname), "");
if (plat.startsWith("x86_64")) cuberiteCache.set("android-x64", { URL: [server, file] });
else if (plat.startsWith("x86")) cuberiteCache.set("android-ia32", { URL: [server, file] });
else if (plat.startsWith("arm64")) cuberiteCache.set("android-arm64", { URL: [server, file] });
else if (plat.startsWith("arm")) cuberiteCache.set("android-arm", { URL: [server, file] });
}
}
break;
}
}));
}
export async function syncCaches() {
await Promise.all([
listMojang(),
listSpigot(),
listPaperProject(),
listPurpurProject(),
listGlowstoneProject(),
listCuberite(),
]);
}

View File

@ -0,0 +1,143 @@
import { extendsFS } from "@sirherobrine23/extends";
import { http } from "@sirherobrine23/http";
import AdmZip from "adm-zip";
import child_process from "node:child_process";
import { createWriteStream } from "node:fs";
import fs from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import readline from "node:readline";
import { finished } from "node:stream/promises";
import tar from "tar";
import { customEvent, defineEvents } from "../../serverRun.js";
import * as javaVersions from "./listVersion.js";
export type platform = "mojang" | "spigot" | "paper" | "purpur" | "glowstone" | "folia" | "cuberite";
export type javaEvents = defineEvents<{
logLine(lineString: string): void;
}>;
/**
* Return boolean if Class input is Java class server
* @param event - Java class
* @returns
*/
export function isJava(event: Java<any>): event is Java<any> {
return event instanceof Java;
}
export class Java<P extends platform> extends customEvent<javaEvents> {
readonly serverFolder: string;
readonly rootServer: string;
readonly platform: P;
constructor(rootServer: string, platform: P) {
super();
if ((!(["mojang", "spigot", "paper", "purpur", "glowstone", "folia", "cuberite"]).includes(platform))) throw new Error("Invalid platform");
this.platform = platform;
this.rootServer = rootServer;
this.serverFolder = path.join(rootServer, "server");
Object.defineProperty(this, "rootServer", { writable: false });
Object.defineProperty(this, "serverFolder", { writable: false });
Object.defineProperty(this, "platform", { writable: false });
}
async installServer(version: string | number) {
const { platform } = this;
if (!(await extendsFS.exists(this.serverFolder))) await fs.mkdir(this.serverFolder, { recursive: true });
if (platform === "mojang") {
if (!javaVersions.mojangCache.size) await javaVersions.listMojang();
await finished((await http.streamRequest(javaVersions.mojangCache.get(version).URL)).pipe(createWriteStream(path.join(this.serverFolder, "server.jar"))));
} else if (platform === "spigot") {
if (!javaVersions.spigotCache.size) await javaVersions.listSpigot();
const spigotRel = javaVersions.spigotCache.get(version);
await finished((await spigotRel.getServer()).pipe(createWriteStream(path.join(this.serverFolder, "server.jar"))));
if (typeof spigotRel.craftbukkit === "function") await finished((await spigotRel.craftbukkit()).pipe(createWriteStream(path.join(this.serverFolder, "craftbukkit.jar"))));
} else if (platform === "paper") {
if (!javaVersions.paperCache.size) await javaVersions.listPaperProject();
await finished((await http.streamRequest(javaVersions.paperCache.get(version).URL)).pipe(createWriteStream(path.join(this.serverFolder, "server.jar"))));
} else if (platform === "purpur") {
if (!javaVersions.purpurCache.size) await javaVersions.listPaperProject();
await finished((await http.streamRequest(javaVersions.purpurCache.get(version).URL)).pipe(createWriteStream(path.join(this.serverFolder, "server.jar"))));
} else if (platform === "glowstone") {
if (!javaVersions.glowstoneCache.size) await javaVersions.listPaperProject();
await finished((await http.streamRequest(javaVersions.glowstoneCache.get(version).URL)).pipe(createWriteStream(path.join(this.serverFolder, "server.jar"))));
} else if (platform === "folia") {
if (!javaVersions.foliaCache.size) await javaVersions.listGlowstoneProject();
await finished((await http.streamRequest(javaVersions.foliaCache.get(version).URL)).pipe(createWriteStream(path.join(this.serverFolder, "server.jar"))));
} else if (platform === "cuberite") {
if (javaVersions.cuberiteCache.size < 3) await javaVersions.listCuberite();
const files = javaVersions.cuberiteCache.get(`${process.platform}-${process.arch}`).URL;
for (const fileURL of files) {
if (fileURL.endsWith(".zip")) {
const tmpFile = path.join(tmpdir(), Date.now() + "_" + path.basename((new URL(fileURL)).pathname));
await finished((await http.streamRequest(fileURL)).pipe(createWriteStream(tmpFile)));
await new Promise<void>((done, reject) => (new AdmZip(tmpFile)).extractAllToAsync(this.serverFolder, true, true, err => err ? reject(err) : done()));
await fs.rm(tmpFile, { force: true });
} else await finished((await http.streamRequest(fileURL)).pipe(tar.extract({cwd: this.serverFolder})));
}
}
}
serverProcess?: child_process.ChildProcess;
async runServer() {
if (this.platform === "cuberite") {
let execPath = path.join(this.serverFolder, "Cuberite");
if (process.platform === "win32") execPath += ".exe";
this.serverProcess = child_process.spawn(execPath, {
cwd: this.serverFolder,
stdio: ["pipe", "pipe", "pipe"],
});
} else {
this.serverProcess = child_process.spawn("java", [
"-XX:+UseG1GC",
"-XX:+ParallelRefProcEnabled",
"-XX:MaxGCPauseMillis=200",
"-XX:+UnlockExperimentalVMOptions",
"-XX:+DisableExplicitGC",
"-XX:+AlwaysPreTouch",
"-XX:G1NewSizePercent=30",
"-XX:G1MaxNewSizePercent=40",
"-XX:G1HeapRegionSize=8M",
"-XX:G1ReservePercent=20",
"-XX:G1HeapWastePercent=5",
"-XX:G1MixedGCCountTarget=4",
"-XX:InitiatingHeapOccupancyPercent=15",
"-XX:G1MixedGCLiveThresholdPercent=90",
"-XX:G1RSetUpdatingPauseTimePercent=5",
"-XX:SurvivorRatio=32",
"-XX:+PerfDisableSharedMem",
"-XX:MaxTenuringThreshold=1",
"-Dusing.aikars.flags=https://mcflags.emc.gs",
"-Daikars.new.flags=true",
"-jar", "server.jar",
], {
cwd: this.serverFolder,
stdio: ["pipe", "pipe", "pipe"],
});
}
// Break line
([
readline.createInterface(this.serverProcess.stdout),
readline.createInterface(this.serverProcess.stderr),
]).forEach(readline => readline.on("error", err => this.emit("error", err)).on("line", line => this.emit("logLine", line)));
this.on("logLine", (line) => {
line = line.replace(/^.*?\[.*\]:/, "").trim();
if (line.startsWith("Starting Minecraft server on")) {
if (line.lastIndexOf(":") === -1) return null;
// const port = Number(line.slice(line.lastIndexOf(":")+1));
// this.emit("portListen", {
// port,
// localListen: "0.0.0.0"
// });
// this.ports.push({
// port,
// localListen: "0.0.0.0"
// });
}
});
return this.serverProcess;
}
}

View File

@ -1,4 +1,9 @@
export * from "./serverManeger.js"; // Bedrock platform
export * as serverManeger from "./serverManeger.js"; export * as Bedrock from "./platform/bedrock/index.js";
export * as Bedrock from "./servers/bedrock.js"; export * as bedrock from "./platform/bedrock/index.js";
export * as Java from "./servers/java.js"; export { isBedrock } from "./platform/bedrock/index.js";
// Java platform
export * as Java from "./platform/java/index.js";
export * as java from "./platform/java/index.js";
export { isJava } from "./platform/java/index.js";

View File

@ -1,317 +0,0 @@
import readline from "node:readline";
import { createWriteStream } from "node:fs";
import { extendsFS } from "@sirherobrine23/extends";
import { pipeline } from "node:stream/promises";
import { format } from "node:util";
import sanitizeFilename from "sanitize-filename";
import child_process from "node:child_process";
import crypto from "node:crypto";
import stream from "node:stream";
import path from "node:path";
import tar from "tar";
import fs from "node:fs/promises";
import os from "node:os";
// Default bds maneger core
const ENVROOT = process.env.BDSCOREROOT || process.env.bdscoreroot;
export const bdsManegerRoot = ENVROOT ? path.resolve(process.cwd(), ENVROOT) : path.join(os.homedir(), ".bdsmaneger");
if (!(await extendsFS.exists(bdsManegerRoot))) await fs.mkdir(bdsManegerRoot, {recursive: true});
export type withPromise<T> = T|Promise<T>;
export interface manegerOptions {
ID?: string,
newID?: boolean,
};
// only letters and numbers
const idReg = /^[a-zA-Z0-9_]+$/;
export interface serverManegerV1 {
id: string,
rootPath: string,
serverFolder: string,
backup: string,
logs: string,
platform: "java"|"bedrock",
runCommand(options: Omit<runOptions, "cwd">): ReturnType<typeof runServer>
};
/**
* Platform path maneger
*/
export async function serverManeger(platform: serverManegerV1["platform"], options: manegerOptions): Promise<serverManegerV1> {
if (!((["java", "bedrock"]).includes(platform))) throw new TypeError("Invalid platform target!");
if (!options) throw new TypeError("Please add serverManeger options!");
const platformFolder = path.join(bdsManegerRoot, platform);
if ((await fs.readdir(platformFolder).then(a => a.length).catch(() => 0)) === 0 && options.newID === undefined) options.newID = true;
// Create or check if exists
if (options.newID === true) {
while(true) {
options.ID = typeof crypto.randomUUID === "function" ? crypto.randomUUID().split("-").join("_") : crypto.randomBytes(crypto.randomInt(8, 14)).toString("hex");
if (!(idReg.test(options.ID))) continue;
if (!((await fs.readdir(platformFolder).catch(() => [])).includes(options.ID))) break;
}
await fs.mkdir(path.join(platformFolder, options.ID), {recursive: true});
} else {
// Test invalid ID
if (String(options.ID).length > 32) throw new TypeError("options.ID is invalid, is very long!");
if (!(!!options.ID && idReg.test(options.ID))) throw new TypeError("options.ID is invalid");
else if (!((await fs.readdir(platformFolder)).includes(options.ID))) throw new Error("ID not exists")
}
// Folders
const rootPath = path.join(platformFolder, path.posix.resolve("/", sanitizeFilename(options.ID)));
const serverFolder = path.join(rootPath, "server");
const backup = path.join(rootPath, "backups");
const log = path.join(rootPath, "logs");
for await (const p of [
serverFolder,
backup,
log,
]) if (!(await extendsFS.exists(p))) await fs.mkdir(p, {recursive: true});
return {
id: options.ID,
platform,
rootPath,
serverFolder,
backup,
logs: log,
async runCommand(options: Omit<runOptions, "cwd">) {
return runServer({...options, cwd: serverFolder});
}
};
}
export async function listIDs(): Promise<{id: string, platform: "bedrock"|"java", delete: () => Promise<void>}[]> {
const main = [];
for await (const platform of ["bedrock", "java"]) {
try {
const platformFolder = path.join(bdsManegerRoot, platform);
if (!(await extendsFS.exists(platformFolder))) continue;
const IDs = await fs.readdir(platformFolder);
for await (const id of IDs) main.push({
id: id,
platform,
async delete() {
return fs.rm(path.join(platformFolder, id), {recursive: true, force: true});
}
});
} catch {}
}
return main;
}
export type portListen = {
port: number,
protocol: "TCP"|"UDP"|"both",
listenOn?: string,
listenFrom?: "server"|"plugin"
};
export type playerAction = {
playerName: string,
onDate: Date,
action: string,
extra?: any
};
export type runOptions = {
cwd: string,
env?: {[k: string]: string|number|boolean},
command: string,
args?: (string|number|boolean)[],
stdio?: child_process.StdioOptions,
paths: serverManegerV1,
serverActions?: {
stop?(this: serverRun): withPromise<void>,
playerAction?(this: serverRun, lineString: string): withPromise<null|void|playerAction>,
hotBackup?(this: serverRun): withPromise<stream.Readable|void>,
portListen?(this: serverRun, lineString: string): withPromise<void|portListen>,
onAvaible?(this: serverRun, lineString: string): withPromise<void|Date>,
postStart?: ((this: serverRun) => withPromise<void>)[],
}
};
export declare class serverRun extends child_process.ChildProcess {
on(event: string, listener: (...args: any[]) => void): this;
once(event: string, listener: (...args: any[]) => void): this;
on(event: "error", listener: (err: Error) => void): this;
once(event: "error", listener: (err: Error) => void): this;
on(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
once(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
on(event: "disconnect", listener: () => void): this;
once(event: "disconnect", listener: () => void): this;
on(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
on(event: "message", listener: (message: child_process.Serializable, sendHandle: child_process.SendHandle) => void): this;
once(event: "message", listener: (message: child_process.Serializable, sendHandle: child_process.SendHandle) => void): this;
on(event: "spawn", listener: () => void): this;
once(event: "spawn", listener: () => void): this;
on(event: "warning", listener: (data: any) => void): this;
once(event: "warning", listener: (data: any) => void): this;
// BDS Assigns
once(event: "line", fn: (data: string, from: "stdout"|"stderr") => void): this;
on(event: "line", fn: (data: string, from: "stdout"|"stderr") => void): this;
once(event: "player", fn: (playerInfo: playerAction) => void): this;
on(event: "player", fn: (playerInfo: playerAction) => void): this;
once(event: "portListening", fn: (portInfo: portListen) => void): this;
on(event: "portListening", fn: (portInfo: portListen) => void): this;
once(event: "serverAvaible", fn: (date: Date) => void): this;
on(event: "serverAvaible", fn: (date: Date) => void): this;
once(event: "backup", fn: (filePath: string) => void): this;
on(event: "backup", fn: (filePath: string) => void): this;
once(event: "hotBackup", fn: (fileStream: stream.Readable) => void): this;
on(event: "hotBackup", fn: (fileStream: stream.Readable) => void): this;
avaibleDate?: Date;
runOptions: runOptions;
portListening: portListen[];
logPath: {stderr: string, stdout: string, merged: string};
playerActions: playerAction[];
stdoutInterface: readline.Interface;
stderrInterface: readline.Interface;
stopServer(): Promise<{code?: number, signal?: NodeJS.Signals}>;
sendCommand(streamPipe: stream.Readable): this;
sendCommand(...args: (string|number|boolean)[]): this;
hotBackup(): this & Promise<Awaited<ReturnType<runOptions["serverActions"]["hotBackup"]>>>;
}
/**
* Run servers globally and hormonally across servers
*/
export async function runServer(options: runOptions): Promise<serverRun> {
if (!options.stdio) options.stdio = ["pipe", "pipe", "pipe"];
const child = child_process.spawn(options.command, [...((options.args ?? []).map(String))], {
// maxBuffer: Infinity,
stdio: options.stdio,
cwd: options.cwd,
env: {
...process.env,
...Object.keys(options.env ?? {}).reduce((acc, a) => {
acc[a] = String(options.env[a]);
return acc;
}, {})
}
}) as serverRun;
child.runOptions = options;
child.portListening = [];
child.playerActions = [];
for (const std of [child.stdout, child.stderr]) if (!std) {
child.kill("SIGKILL");
throw new TypeError("Stdout or Stderr stream disabled, killed process, cannot continue to exec server, check stdio passed to spawn!");
}
// Log Write
const currentDate = new Date();
const baseLog = path.join(options.paths.logs, format("%s_%s_%s_%s-%s-%s", currentDate.getDate(), currentDate.getMonth()+1, currentDate.getFullYear(), currentDate.getHours(), currentDate.getMinutes(), currentDate.getSeconds()));
await fs.mkdir(baseLog, {recursive: true});
child.logPath = {stdout: path.join(baseLog, "stdout.log"), stderr: path.join(baseLog, "stderr.log"), merged: path.join(baseLog, "server.log")};
const allLog = createWriteStream(child.logPath.merged);
child.stdout.pipe(allLog);
child.stdout.pipe(createWriteStream(child.logPath.stdout));
child.stderr.pipe(allLog);
child.stderr.pipe(createWriteStream(child.logPath.stderr));
// Lines
const stdout = child.stdoutInterface = readline.createInterface(child.stdout).on("line", data => child.emit("line", data, "stdout")).on("error", err => child.emit("error", err));
const stderr = child.stderrInterface = readline.createInterface(child.stderr).on("line", data => child.emit("line", data, "stderr")).on("error", err => child.emit("error", err));
if (typeof options.serverActions?.playerAction === "function") {
for (const std of [stdout, stderr]) std.on("line", async data => {
const playerData = await Promise.resolve(options.serverActions.playerAction.call(child, data) as ReturnType<typeof options.serverActions.playerAction>);
if (!playerData) return;
child.playerActions.push(playerData);
child.emit("player", playerData);
});
}
if (typeof options.serverActions?.portListen === "function") {
for (const std of [stdout, stderr]) std.on("line", async data => {
const portData = await Promise.resolve(options.serverActions.portListen.call(child, data) as ReturnType<typeof options.serverActions.portListen>);
if (!portData) return;
portData.listenFrom ??= "server";
child.portListening.push(portData);
child.emit("portListening", portData);
});
}
child.sendCommand = function (...args) {
if (!child.stdin.writable) {
child.emit("error", new Error("cannot send command to server"));
return child;
};
if (args[0] instanceof stream.Readable) {
args[0].on("data", data => child.stdin.write(data)).once("close", () => child.stdin.write("\n"));
return child;
}
child.stdin.write(args.map(String).join(" ")+"\n");
return child;
}
child.stopServer = async function () {
child.sendCommand("");
const stop = options.serverActions?.stop ?? function () {
child.kill("SIGINT");
const kill = setTimeout(() => {
clearTimeout(kill);
if (child.exitCode !== null) return;
child.kill("SIGKILL");
}, 2500);
};
Promise.resolve().then(() => stop.call(child)).catch(err => child.emit("error", err));
return new Promise((done, reject) => child.once("error", reject).once("exit", (code, signal) => done({code, signal})));
}
child.hotBackup = function hotBackup() {
return Object.assign({}, Promise.resolve().then((async () => {
if (!options.serverActions?.hotBackup) throw new Error("Hot backup disabled to current platform!");
child.emit("backup", "start");
return Promise.resolve(options.serverActions.hotBackup.call(child) as ReturnType<typeof options.serverActions.hotBackup>).then(data => {
child.emit("backup", "success");
return data;
}).catch(err => {
child.emit("backup", "fail");
return Promise.reject(err);
});
})), child);
}
if (typeof options.serverActions?.onAvaible === "function") {
let run = options.serverActions.onAvaible;
for (const std of [stdout, stderr]) std.on("line", async data => {
if (!run) return null;
const avaibleDate = await Promise.resolve(run.call(child, data) as ReturnType<typeof run>);
if (!avaibleDate) return;
child.avaibleDate = avaibleDate;
if (options.serverActions?.postStart) for (const ss of options.serverActions?.postStart) Promise.resolve().then(() => ss.call(child)).catch(err => child.emit("error", err));
});
} else if (options.serverActions?.postStart?.length > 0) child.emit("warning", "no post actions run!");
child.once("close", async () => {
const cDate = new Date();
const month = String(cDate.getMonth()+1 > 9 ? cDate.getMonth()+1 : "0"+(cDate.getMonth()+1).toString());
const day = String(cDate.getDate() > 9 ? cDate.getDate() : "0"+((cDate.getDate()).toString()));
const backupFile = path.join(options.paths.backup, String(cDate.getFullYear()), month, day, `${cDate.getHours()}_${cDate.getMinutes()}.tgz`);
try {
if (!(await extendsFS.exists(path.dirname(backupFile)))) await fs.mkdir(path.dirname(backupFile), {recursive: true});
const ff = await fs.readdir(options.paths.serverFolder);
await pipeline(tar.create({
gzip: true,
cwd: options.paths.serverFolder,
prefix: ""
}, ff), createWriteStream(backupFile));
child.emit("backup", backupFile);
} catch (err) {
if (await extendsFS.exists(backupFile)) await fs.unlink(backupFile);
child.emit("error", err);
}
});
return child;
}

View File

@ -0,0 +1,70 @@
import EventEmitter from "node:events";
export type EventMap = Record<string, (...args: any[]) => void>;
export type defineEvents<T extends EventMap> = T;
type EventKey<T extends EventMap> = string & keyof T;
export interface customEvent<T extends EventMap> extends EventEmitter {
emit<K extends EventKey<T>>(eventName: K, ...args: Parameters<T[K]>): boolean;
emit(name: "error", err: Error): boolean;
on<K extends EventKey<T>>(eventName: K, fn: T[K]): this;
on(eventName: "error", fn: (err: Error) => void): this;
prependListener<K extends EventKey<T>>(eventName: K, fn: T[K]): this;
prependListener(eventName: "error", fn: (err: Error) => void): this;
once<K extends EventKey<T>>(eventName: K, fn: T[K]): this;
once(eventName: "error", fn: (err: Error) => void): this;
prependOnceListener<K extends EventKey<T>>(eventName: K, fn: T[K]): this;
prependOnceListener(eventName: "error", fn: (err: Error) => void): this;
removeListener<K extends EventKey<T>>(eventName: K, fn: T[K]): this;
removeListener(eventName: "error", fn: (err: Error) => void): this;
off<K extends EventKey<T>>(eventName: K, fn: T[K]): this;
off(eventName: "error", fn: (err: Error) => void): this;
removeAllListeners<K extends EventKey<T>>(eventName: K): this;
removeAllListeners(eventName: "error"): this;
rawListeners<K extends EventKey<T>>(eventName: K): (T[K])[];
rawListeners(eventName: "error"): ((err: Error) => void)[];
eventNames(): (EventKey<T> | "error")[];
}
export class customEvent<T extends EventMap> extends EventEmitter {
constructor() {
super({captureRejections: true});
}
};
export class versionsStorages<T> extends Map<string, T> {
constructor(origem: Record<string, T> = {}) {
super(Object.keys(origem).map(key => ([key, origem[key]])));
}
prettyVersion(serverVersion: string|number): string {
const checkIsNumber = (arg0: string|number) => typeof arg0 === "number" ? arg0 : Number(arg0).toString() === arg0 ? Number(arg0) : arg0;
serverVersion = checkIsNumber(serverVersion);
if (typeof serverVersion === "number") return Array.from(this.keys()).at(serverVersion);
return serverVersion;
}
get(serverVersion: string|number): T {
return super.get(this.prettyVersion(serverVersion));
}
has(serverVersion: string|number): boolean {
return super.has(this.prettyVersion(serverVersion));
}
toJSON() {
return Array.from(super.keys()).reduce<{[version: string]: T}>((acc, key) => {
acc[key] = super.get(key);
return acc;
}, {});
}
}

View File

@ -1,388 +0,0 @@
import fsOld, { promises as fs } from "node:fs";
import coreHttp, { Github } from "@sirherobrine23/http";
import { runOptions, serverManegerV1 } from "../serverManeger.js";
import { oracleStorage } from "../internal.js";
import { pipeline } from "node:stream/promises";
import { Readable } from "node:stream";
import extendsFS, { promiseChildProcess } from "@sirherobrine23/extends";
import semver from "semver";
import unzip from "unzipper";
import utils from "node:util";
import path from "node:path";
import tar from "tar";
export interface bedrockOptions {
/**
* Alternative server instead of official Mojang server
*/
altServer?: "mojang"|"pocketmine"|"powernukkit"|"nukkit"|"cloudbust",
};
const pocketmineGithub = await Github.repositoryManeger("pmmp", "PocketMine-MP");
export type bedrockList = {
date: Date,
version: string,
release: "preview"|"stable",
downloads: {
php?: {
installPHP(serverPath: serverManegerV1): Promise<void>,
},
server: {
getServer(): Promise<Readable>,
url?: string,
urls?: {
[platform in NodeJS.Platform]?: {
[arch in NodeJS.Architecture]?: string
}
}
[K: string]: any
}
}
};
/**
* List Minecrft bedrock server versions
*
* @param altServer - Alternative server of official Mojang
* @returns
*/
export async function listVersions(altServer?: bedrockOptions["altServer"]): Promise<bedrockList[]> {
if (!altServer) altServer = "mojang";
if (altServer) if (!(["mojang", "cloudbust", "cloudbust", "nukkit", "pocketmine", "powernukkit"]).includes(altServer)) throw new TypeError("Invalid alt server");
if (altServer === "pocketmine") {
return (await pocketmineGithub.release.getRelease()).filter(rel => (rel.assets.find(assert => assert.name.endsWith(".phar")) ?? {}).browser_download_url).map(rel => ({
date: new Date(rel.created_at),
version: rel.tag_name,
release: rel.prerelease ? "preview" : "stable",
downloads: {
php: {
async installPHP(serverPath: serverManegerV1) {
const phpFile = (await oracleStorage.listFiles("php_bin")).find(file => file.name.includes(process.platform) && file.name.includes(process.arch));
if (!phpFile) throw new Error(`Unable to find php files for ${process.platform} with architecture ${process.arch}`);
if (phpFile.name.endsWith(".tar.gz")||phpFile.name.endsWith(".tgz")||phpFile.name.endsWith(".tar")) await pipeline(oracleStorage.getFile(phpFile.name), tar.extract({cwd: serverPath.serverFolder}));
else if (phpFile.name.endsWith(".zip")) await pipeline(oracleStorage.getFile(phpFile.name), unzip.Extract({path: serverPath.serverFolder}));
else throw new Error("Found file is not supported!");
return null
},
},
server: {
url: (rel.assets.find(assert => assert.name.endsWith(".phar")) ?? {}).browser_download_url,
async getServer() {
const pharFile = rel.assets.find(assert => assert.name.endsWith(".phar"));
if (!pharFile) throw new Error("Version not includes server file!");
return coreHttp.streamRequest(pharFile.browser_download_url);
}
}
}
}));
} else if (altServer === "powernukkit") {
const releases_version = (await coreHttp.jsonRequest<{[k: string]: {version: string, releaseTime: number, minecraftVersion: string, artefacts: string[], commitId: string, snapshotBuild?: number}[]}>("https://raw.githubusercontent.com/PowerNukkit/powernukkit-version-aggregator/master/powernukkit-versions.json")).body;
return Object.keys(releases_version).reduce((acc, key) => acc.concat(releases_version[key]), [] as (typeof releases_version)[string]).map(data => {
const dt = new Date(data.releaseTime);
const getArtefactExtension = (artefactId: string) => (artefactId.includes("REDUCED_JAR")) ? ".jar" : (artefactId.includes("REDUCED_SOURCES_JAR")) ? "-sources.jar" : (artefactId.includes("SHADED_JAR")) ? "-shaded.jar" : (artefactId.includes("SHADED_SOURCES_JAR")) ? "-shaded-sources.jar" : (artefactId.includes("JAVADOC_JAR")) ? "-javadoc.jar" : ".unknown";
function buildArtefactUrl(data: any, artefactId?: string) {
const buildReleaseArtefactUrl = (data: any, artefactId?: string) => !data.artefacts.includes(artefactId) ? null : utils.format("https://search.maven.org/remotecontent?filepath=org/powernukkit/powernukkit/%s/powernukkit-%s%s", data.version, data.version, getArtefactExtension(artefactId));
const buildSnapshotArtefactUrl = (data: any, artefactId?: string) => !data.artefacts.includes(artefactId) ? null : utils.format("https://oss.sonatype.org/content/repositories/snapshots/org/powernukkit/powernukkit/%s-SNAPSHOT/powernukkit-%s-%s%s", data.version.substring(0, data.version.indexOf("-SNAPSHOT")), data.version.substring(0, data.version.indexOf("-SNAPSHOT")), dt.getUTCFullYear().toString().padStart(4, "0") + (dt.getUTCMonth() + 1).toString().padStart(2, "0") + dt.getUTCDate().toString().padStart(2, "0") + "." + dt.getUTCHours().toString().padStart(2, "0") + dt.getUTCMinutes().toString().padStart(2, "0") + dt.getUTCSeconds().toString().padStart(2, "0") + "-" + data.snapshotBuild, getArtefactExtension(artefactId));
if (artefactId == "GIT_SOURCE") {
if (data.commitId) return utils.format("https://github.com/PowerNukkit/PowerNukkit/tree/%s", data.commitId);
else if (data.snapshotBuild && data.artefacts.includes("SHADED_SOURCES_JAR")) return buildSnapshotArtefactUrl(data, "SHADED_SOURCES_JAR");
else if (data.snapshotBuild && data.artefacts.includes("REDUCED_SOURCES_JAR")) return buildSnapshotArtefactUrl(data, "REDUCED_SOURCES_JAR");
else if (data.artefacts.includes("SHADED_SOURCES_JAR")) return buildReleaseArtefactUrl(data, "SHADED_SOURCES_JAR");
else if (data.artefacts.includes("REDUCED_SOURCES_JAR")) return buildReleaseArtefactUrl(data, "REDUCED_SOURCES_JAR");
} else if (data.snapshotBuild) return buildSnapshotArtefactUrl(data, artefactId);
else return buildReleaseArtefactUrl(data, artefactId);
return null;
}
const artefacts = data.artefacts.reduce((acc, artefactId) => {acc[artefactId] = buildArtefactUrl(data, artefactId); return acc;}, {} as {[key: string]: string});
return {
date: dt,
version: data.version,
release: data.snapshotBuild ? "stable" : "preview",
downloads: {
server: {
mcpeVersion: data.minecraftVersion,
url: artefacts.SHADED_JAR || artefacts.REDUCED_JAR,
async getServer() {
if (!(artefacts.SHADED_JAR || artefacts.REDUCED_JAR)) throw new Error("Cannot get server file to the version!");
return coreHttp.streamRequest(artefacts.SHADED_JAR || artefacts.REDUCED_JAR)
},
}
}
};
});
} else if (altServer === "cloudbust"||altServer === "nukkit") {
const { body: { jobs } } = await coreHttp.jsonRequest<{jobs: {name: string, _class: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${altServer === "nukkit" ? "Nukkit" : "Server"}/api/json`);
const buildFiles = await Promise.all(jobs.filter(b => b._class === "org.jenkinsci.plugins.workflow.job.WorkflowJob").map(b => b.name).map(async branch => {
const { body: { builds } } = await coreHttp.jsonRequest<{builds: {_class: string, number: number, url: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${altServer === "nukkit" ? "Nukkit" : "Server"}/job/${branch}/api/json`);
return Promise.all(builds.map(async build => {
const { body: { artifacts, result, timestamp } } = await coreHttp.jsonRequest<{result: "SUCCESS", timestamp: number, artifacts: {displayPath: string, fileName: string, relativePath: string}[]}>(`https://ci.opencollab.dev/job/NukkitX/job/${altServer === "nukkit" ? "Nukkit" : "Server"}/job/${branch}/${build.number}/api/json`);
if (result !== "SUCCESS") return [];
return artifacts.filter(f => f.relativePath.endsWith(".jar")).map(target => ({
buildNumber: build.number,
branch,
releaseDate: new Date(timestamp),
url: `https://ci.opencollab.dev/job/NukkitX/job/${altServer === "nukkit" ? "Nukkit" : "Server"}/job/${branch}/${build.number}/artifact/${target.relativePath}`,
}));
}));
})).then(r => r.flat(2));
return buildFiles.sort((b, a) => a.releaseDate.getTime() - b.releaseDate.getTime()).map(rel => ({
date: rel.releaseDate,
release: "preview",
version: `${rel.branch}_${rel.buildNumber}`,
downloads: {
server: {
url: rel.url,
async getServer() {
return coreHttp.streamRequest(rel.url);
},
}
}
}));
} else if (altServer === "mojang") {
return (await coreHttp.jsonRequest<{version: string, date: Date, release?: "stable"|"preview", url: {[platform in NodeJS.Platform]?: {[arch in NodeJS.Architecture]?: string}}}[]>("https://sirherobrine23.github.io/BedrockFetch/all.json")).body.sort((b, a) => semver.compare(semver.valid(semver.coerce(a.version)), semver.valid(semver.coerce(b.version)))).map(rel => ({
version: rel.version,
date: new Date(rel.date),
release: rel.release === "preview" ? "preview" : "stable",
downloads: {
server: {
url: rel.url[process.platform]?.[process.arch],
async getServer() {
const platformURL = (rel.url[process.platform] ?? rel.url["linux"]);
if (!platformURL) throw new Error("Cannot get platform URL");
const arch = platformURL[process.arch] ?? platformURL["x64"];
if (!arch) throw new Error("Cannot get bedrock server to current arch");
return coreHttp.streamRequest(arch);
},
urls: rel.url
}
}
}));
} else throw new Error("Invalid platform");
}
export async function installServer(serverPath: serverManegerV1, options: bedrockOptions & {version?: string, allowBeta?: boolean}) {
const versions = await listVersions(options?.altServer);
if (!options.altServer) options.altServer = "mojang";
if (options.altServer === "pocketmine") {
const rel = options.version === "latest" ? versions.at(0) : versions.find(rel => rel.version === options.version);
if (!rel) throw new Error("Version not exsists");
await rel.downloads.php.installPHP(serverPath);
await pipeline(await rel.downloads.server.getServer(), fsOld.createWriteStream(path.join(serverPath.serverFolder, "server.phar")));
return {
...rel,
id: serverPath.id,
};
} else if (options.altServer === "cloudbust" || options.altServer === "powernukkit" || options.altServer === "nukkit") {
if ((["cloudbust", "nukkit"]).includes(options.altServer)) options.version = "latest";
const rel = options.version === "latest" ? versions.at(0) : versions.find(rel => rel.version === options.version);
if (!rel) throw new Error("Version not exists");
await pipeline(await rel.downloads.server.getServer(), fsOld.createWriteStream(path.join(serverPath.serverFolder, "server.jar")));
return {
...rel,
id: serverPath.id,
};
} else if (options.altServer === "mojang") {
const bedrockVersion = versions.find(rel => {
if (rel.release === "preview") if (options.allowBeta !== true) return false;
const version = (options.version ?? "latest").trim();
if (version.toLowerCase() === "latest") return true;
return rel.version === version;
});
if (!bedrockVersion) throw new Error("Não existe essa versão");
let downloadUrl = bedrockVersion.downloads.server.url;
if ((["android", "linux"] as NodeJS.Process["platform"][]).includes(process.platform) && process.arch !== "x64") {
if (!downloadUrl) {
for (const emu of ["qemu-x86_64-static", "qemu-x86_64", "box64"]) {
if (downloadUrl) break;
if (await promiseChildProcess.commandExists(emu)) downloadUrl = bedrockVersion.downloads.server.urls.linux?.x64;
}
}
}
if (!downloadUrl) throw new Error(`Não existe o URL de download para ${process.platform} na arquitetura ${process.arch}`);
const filesBackup = ["server.properties", "valid_known_packs.json", "permissions.json", "allowlist.json", "whitelist.json"];
const datS = (await Promise.all(filesBackup.map(async f => !await extendsFS.exists(path.join(serverPath.serverFolder, f)) ? null : ({path: f, data: await fs.readFile(path.join(serverPath.serverFolder, f))})))).filter(a => !!a);
await pipeline(await coreHttp.streamRequest(downloadUrl), unzip.Extract({path: serverPath.serverFolder}));
await Promise.all(datS.map(async f => fs.writeFile(f.path, f.data)));
return {
...bedrockVersion,
id: serverPath.id,
};
} else throw new Error("Invalid platform");
}
export async function startServer(maneger: serverManegerV1, options: bedrockOptions) {
if (!options.altServer) options.altServer = "mojang";
if (options.altServer === "powernukkit"||options.altServer === "cloudbust") {
return maneger.runCommand({
command: "java",
args: [
"-XX:+UseG1GC",
"-XX:+ParallelRefProcEnabled",
"-XX:MaxGCPauseMillis=200",
"-XX:+UnlockExperimentalVMOptions",
"-XX:+DisableExplicitGC",
"-XX:+AlwaysPreTouch",
"-XX:G1NewSizePercent=30",
"-XX:G1MaxNewSizePercent=40",
"-XX:G1HeapRegionSize=8M",
"-XX:G1ReservePercent=20",
"-XX:G1HeapWastePercent=5",
"-XX:G1MixedGCCountTarget=4",
"-XX:InitiatingHeapOccupancyPercent=15",
"-XX:G1MixedGCLiveThresholdPercent=90",
"-XX:G1RSetUpdatingPauseTimePercent=5",
"-XX:SurvivorRatio=32",
"-XX:+PerfDisableSharedMem",
"-XX:MaxTenuringThreshold=1",
"-Dusing.aikars.flags=https://mcflags.emc.gs",
"-Daikars.new.flags=true",
"-jar", "server.jar",
],
paths: maneger,
serverActions: {
stop() {
this.sendCommand("stop");
},
}
})
} else if (options.altServer === "pocketmine") {
return maneger.runCommand({
command: (await extendsFS.readdir(maneger.serverFolder)).find(file => file.endsWith("php")||file.endsWith("php.exe")),
args: [
"server.phar",
"--no-wizard"
],
paths: maneger,
serverActions: {
stop() {
this.sendCommand("stop")
},
}
});
}
if (process.platform === "darwin") throw new Error("Run in docker or podman!");
const run: Omit<runOptions, "cwd"> = {
command: path.join(maneger.serverFolder, "bedrock_server"+(process.platform === "win32" ? ".exe" : "")),
paths: maneger,
serverActions: {
stop() {
this.sendCommand("stop");
},
portListen(lineString) {
// [INFO] IPv4 supported, port: 19132
// [2023-03-08 13:01:57 INFO] Listening on IPv4 port: 19132
const ipProtocol = lineString.slice(lineString.indexOf("IPv"), lineString.indexOf("IPv")+4);
if (ipProtocol) {
let port = lineString.slice(lineString.lastIndexOf("port:")+5).trim();
if (port.indexOf(":") !== -1) port = port.slice(0, port.lastIndexOf(":"));
return {
protocol: "UDP",
listenOn: ipProtocol.toLowerCase() === "ipv4" ? "0.0.0.0" : "[::]",
port: Number(port),
};
}
return null;
},
playerAction(lineString) {
lineString = lineString.replace(/^(.*)?\[.*\]/, "").trim();
if (lineString.startsWith("Player")) {
lineString = lineString.replace("Player", "").trim();
// Spawned, disconnected, connected
let action: string;
if (lineString.startsWith("Spawned")) action = "Spawned";
else if (lineString.startsWith("disconnected")) action = "disconnected";
else if (lineString.startsWith("connected")) action = "connected";
if (!action) return null;
lineString = lineString.replace(action, "").trim();
if (lineString.startsWith(":")) lineString = lineString.slice(1).trim();
let playerName = lineString.substring(0, lineString.indexOf("xuid:")-1).trim();
if (!playerName) return null;
if (playerName.endsWith(",")) playerName = playerName.slice(0, playerName.length - 1);
let xuid: string;
if (lineString.indexOf("xuid:") !== -1) {
xuid = lineString.slice(lineString.indexOf("xuid:")+5).trim();
if (!xuid) xuid = null;
}
return {
onDate: new Date(),
action,
playerName,
extra: {
xuid
}
};
};
return null;
},
onAvaible(data) {
// [2023-03-06 21:37:27:699 INFO] Server started.
data = data.replace(/^.*?\[.*\]/, "").trim();
if (data.includes("started") && data.includes("Server")) return new Date();
return null
},
async hotBackup() {
const ff = (await fs.readdir(this.runOptions.paths.serverFolder)).filter(ff => {
let ok = ff.endsWith(".json");
if (!ok) ok = ff === "server.properties";
if (!ok) ok = ff === "worlds";
return ok;
});
return tar.create({
gzip: true,
cwd: this.runOptions.paths.serverFolder,
prefix: ""
}, ff);
},
postStart: [
async function() {
let breaked = false;
this.once("close", () => breaked = true);
let w: any;
this.once("close", () => w.close());
while(true) {
if (breaked) break;
await new Promise(done => {
this.once("close", done);
w = fsOld.watch(this.runOptions.paths.serverFolder, {recursive: true}, () => {w.close(); done(null);}).on("error", () => {});
});
await new Promise(done => setTimeout(done, 1000));
const cDate = new Date();
const month = String(cDate.getMonth()+1 > 9 ? cDate.getMonth()+1 : "0"+(cDate.getMonth()+1).toString());
const day = String(cDate.getDate() > 9 ? cDate.getDate() : "0"+((cDate.getDate()).toString()));
const backupFile = path.join(this.runOptions.paths.backup, "hotBackup", String(cDate.getFullYear()), month, day, `${cDate.getHours()}_${cDate.getMinutes()}.tgz`);
if (!(await extendsFS.exists(path.dirname(backupFile)))) await fs.mkdir(path.dirname(backupFile), {recursive: true});
const ff = (await fs.readdir(this.runOptions.paths.serverFolder)).filter(ff => {
let ok = ff.endsWith(".json");
if (!ok) ok = ff === "server.properties";
if (!ok) ok = ff === "worlds";
return ok;
});
const hotTar = tar.create({
gzip: true,
cwd: this.runOptions.paths.serverFolder,
prefix: ""
}, ff);
this.emit("hotBackup", hotTar);
await pipeline(hotTar, fsOld.createWriteStream(backupFile));
}
}
]
}
};
if ((["android", "linux"] as NodeJS.Process["platform"][]).includes(process.platform) && process.arch !== "x64") {
for (const emu of ["qemu-x86_64-static", "qemu-x86_64", "box64"]) {
if (await promiseChildProcess.commandExists(emu)) {
run.args = [run.command, ...run.args];
run.command = emu;
break;
}
}
}
return maneger.runCommand(run);
}

View File

@ -1,237 +0,0 @@
import { serverManegerV1 } from "../serverManeger.js";
import { oracleStorage } from "../internal.js";
import { extendsFS } from "@sirherobrine23/extends";
import { pipeline } from "node:stream/promises";
import coreHttp, { Github } from "@sirherobrine23/http";
import semver from "semver";
import stream from "node:stream";
import utils from "node:util";
import path from "node:path";
import fs from "node:fs";
export interface javaOptions {
/**
* Alternative server instead of official Mojang server
*/
altServer?: "mojang"|"spigot"|"paper"|"purpur"|"glowstone"|"folia"|"cuberite"
};
export type javaList = {
version: string,
release: "stable"|"snapshot",
date?: Date,
platform?: NodeJS.Platform;
architecture?: NodeJS.Architecture;
getFile: {
fileName: string;
stream(): Promise<stream.Readable>;
fileURL?: string;
}[],
};
export async function listVersions(altServer?: javaOptions["altServer"]): Promise<javaList[]> {
if (!altServer) altServer = "mojang";
if (altServer) if(!(["mojang", "paper", "folia", "purpur", "spigot", "glowstone", "cuberite"]).includes(altServer)) throw new TypeError("Invalid alt server!");
if (altServer === "purpur") {
return (await Promise.all((await coreHttp.jsonRequest<{versions: string[]}>("https://api.purpurmc.org/v2/purpur")).body.versions.map(async (version): Promise<javaList> => ({
version,
release: "stable",
date: new Date((await coreHttp.jsonRequest<{timestamp: number}>(utils.format("https://api.purpurmc.org/v2/purpur/%s/latest", version))).body.timestamp),
getFile: [
{
fileName: "server.jar",
fileURL: utils.format("https://api.purpurmc.org/v2/purpur/%s/latest/download", version),
async stream() {return coreHttp.streamRequest(utils.format("https://api.purpurmc.org/v2/purpur/%s/latest/download", version));},
}
],
})))).sort((b, a) => semver.compare(semver.valid(semver.coerce(a.version)), semver.valid(semver.coerce(b.version))));
} else if (altServer === "paper" || altServer === "folia") {
const uriPATH = ["/v2/projects", altServer];
return (await Promise.all((await coreHttp.jsonRequest<{versions: string[]}>(new URL(uriPATH.join("/"), "https://api.papermc.io"))).body.versions.map(async (version): Promise<javaList> => {
const build = (await coreHttp.jsonRequest<{builds: number[]}>(new URL(uriPATH.concat(["versions", version]).join("/"), "https://api.papermc.io"))).body.builds.at(-1);
const data = (await coreHttp.jsonRequest<{time: string, downloads: {[k: string]: {name: string, sha256: string}}}>(new URL(uriPATH.concat(["versions", version, "builds", build.toString()]).join("/"), "https://api.papermc.io"))).body;
const fileUrl = new URL(uriPATH.concat(["versions", version, "builds", build.toString(), "downloads", data.downloads["application"].name]).join("/"), "https://api.papermc.io");
return {
version,
date: new Date(data.time),
release: "stable",
getFile: [{
fileURL: fileUrl.toString(),
fileName: "server.jar",
async stream() {return coreHttp.streamRequest(fileUrl);}
}]
}
}))).sort((b, a) => semver.compare(semver.valid(semver.coerce(a.version)), semver.valid(semver.coerce(b.version))));
} else if (altServer === "spigot") {
const fileList = await oracleStorage.listFiles("SpigotBuild");
return fileList.filter(f => f.name.endsWith(".jar") && !f.name.includes("craftbukkit-")).map((file): javaList => {
let version: string;
if (!(version = semver.valid(semver.coerce(file.name.replace("SpigotBuild/", "").replace(".jar", ""))))) return null;
const craftBuckit = ([...fileList]).reverse().find(file => file.name.startsWith("SpigotBuild/craftbukkit-"+version) || file.name.startsWith("SpigotBuild/craftbukkit-"+(version.endsWith(".0") ? version.slice(0, -2) : version)));
return {
release: "stable",
version,
getFile: [
...(craftBuckit ? [{
fileName: path.basename(craftBuckit.name),
async stream() {
return craftBuckit.getFile();
},
}] : []),
{
fileName: "server.jar",
async stream() {
return file.getFile();
},
},
],
};
}).filter(rel => !!rel).sort((b, a) => semver.compare(semver.valid(semver.coerce(a.version)), semver.valid(semver.coerce(b.version))));
} else if (altServer === "glowstone") {
const repo = await Github.repositoryManeger("GlowstoneMC", "Glowstone");
const rels = await repo.release.getRelease();
return rels.filter(rel => !!rel.assets.find(asset => asset.name.endsWith(".jar"))).map(rel => ({
version: rel.tag_name,
release: "stable",
fileUrl: rel.assets.find(asset => asset.name.endsWith(".jar")).browser_download_url,
getFile: [{
fileURL: rel.assets.find(asset => asset.name.endsWith(".jar")).browser_download_url,
fileName: "server.jar",
async stream() {
return coreHttp.streamRequest(rel.assets.find(asset => asset.name.endsWith(".jar")).browser_download_url);
},
}]
}));
} else if (altServer === "cuberite") {
const buildFiles: {buildNumber: number; project: string; releaseDate: Date; url: string;}[] = [];
for (const project of ["android", "linux-aarch64", "linux-armhf", "linux-i386", "linux-x86_64", "darwin-x86_64"]) {
const { builds = [] } = await coreHttp.jsonRequestBody<{builds: {number: number, _class: string}[]}>(`https://builds.cuberite.org/job/${project}/api/json`);
await Promise.all(builds.slice(0, 16).map(async job => {
const { artifacts = [], result, timestamp } = await coreHttp.jsonRequestBody<{timestamp: number, result: string, artifacts: {relativePath: string, fileName: string}[]}>(`https://builds.cuberite.org/job/${project}/${job.number}/api/json`);
if (result !== "SUCCESS") return;
artifacts.filter(file => !file.fileName.endsWith(".sha1")).forEach(file => buildFiles.push({
project,
url: `https://builds.cuberite.org/job/${project}/${job.number}/artifact/${file.relativePath}`,
buildNumber: job.number,
releaseDate: new Date(timestamp),
}));
}));
}
return buildFiles.concat([
{
buildNumber: -1,
project: "windows-x86_64",
releaseDate: new Date(),
url: "https://download.cuberite.org/windows-x86_64/Cuberite.zip",
},
{
buildNumber: -1,
project: "windows-x86",
releaseDate: new Date(),
url: "https://download.cuberite.org/windows-i386/Cuberite.zip",
},
]).sort((b, a) => a.releaseDate.getTime() - b.releaseDate.getTime()).map(rel => ({
date: rel.releaseDate,
version: `${rel.project}_${rel.buildNumber}`,
platform: rel.project.startsWith("windows") ? "win32" : rel.project.startsWith("linux") ? "linux" : rel.project.startsWith("android") ? "android" : rel.project.startsWith("darwin") ? "darwin" : undefined,
architecture: rel.project.endsWith("x86_64") ? "x64" : rel.project.endsWith("i386") ? "ia32" : rel.project.endsWith("armhf") ? "arm" : rel.project.endsWith("aarch64") ? "arm64" : undefined,
release: "stable",
fileUrl: rel.url,
getFile: [{
fileName: "server.jar",
async stream() {
return coreHttp.streamRequest(rel.url);
},
}]
}));
} else if (altServer === "mojang") {
return (await Promise.all((await coreHttp.jsonRequest<{versions: {id: string, releaseTime: string, url: string, type: "snapshot"|"release"}[]}>("https://launchermeta.mojang.com/mc/game/version_manifest_v2.json")).body.versions.map(async (data): Promise<javaList> => {
const fileURL = (await coreHttp.jsonRequest<{downloads: {[k: string]: {size: number, url: string}}}>(data.url)).body.downloads?.["server"]?.url;
if (!fileURL) return null;
return {
version: data.id,
date: new Date(data.releaseTime),
release: data.type === "snapshot" ? "snapshot" : "stable",
getFile: [{
fileName: "server.jar",
async stream() {
return coreHttp.streamRequest(fileURL);
},
}],
};
}))).filter(a => !!a);
} else throw new Error("Invalid platform");
}
export async function installServer(serverPath: serverManegerV1, options: javaOptions & {version?: string, allowBeta?: boolean}) {
if (!options.altServer) options.altServer = "mojang";
const version = (await listVersions(options.altServer)).filter(rel => rel.release === "stable" ? true : !!options.allowBeta).find(rel => (!options.version || options.version === "latest" || rel.version === options.version));
if (!version) throw new Error("The specified version does not exist!");
for (const file of version.getFile) await pipeline(await file.stream(), fs.createWriteStream(path.join(serverPath.serverFolder, file.fileName)));
await fs.promises.writeFile(path.join(serverPath.serverFolder, "eula.txt"), "eula=true\n");
return {
id: serverPath.id,
version: version.version,
release: version.release,
date: version.date,
};
}
export async function startServer(serverPath: serverManegerV1, options: javaOptions) {
if (!options.altServer) options.altServer = "mojang";
// Java server
if (await extendsFS.exists(path.join(serverPath.serverFolder, "server.jar"))) {
return serverPath.runCommand({
command: "java",
args: [
"-XX:+UseG1GC",
"-XX:+ParallelRefProcEnabled",
"-XX:MaxGCPauseMillis=200",
"-XX:+UnlockExperimentalVMOptions",
"-XX:+DisableExplicitGC",
"-XX:+AlwaysPreTouch",
"-XX:G1NewSizePercent=30",
"-XX:G1MaxNewSizePercent=40",
"-XX:G1HeapRegionSize=8M",
"-XX:G1ReservePercent=20",
"-XX:G1HeapWastePercent=5",
"-XX:G1MixedGCCountTarget=4",
"-XX:InitiatingHeapOccupancyPercent=15",
"-XX:G1MixedGCLiveThresholdPercent=90",
"-XX:G1RSetUpdatingPauseTimePercent=5",
"-XX:SurvivorRatio=32",
"-XX:+PerfDisableSharedMem",
"-XX:MaxTenuringThreshold=1",
"-Dusing.aikars.flags=https://mcflags.emc.gs",
"-Daikars.new.flags=true",
"-jar", "server.jar",
"--nogui",
// Save world in worlds folder
"--universe", "worlds",
// ...extraArgs,
],
paths: serverPath,
serverActions: {
stop() {
this.sendCommand("stop");
},
portListen(lineString) {
// [21:19:18] [Server thread/INFO]: Starting Minecraft server on *:25565
lineString = lineString.replace(/^.*?\[.*\]:/, "").trim();
if (lineString.startsWith("Starting Minecraft server on")) {
if (lineString.lastIndexOf(":") === -1) return null;
const port = Number(lineString.slice(lineString.lastIndexOf(":")+1));
return {
port,
protocol: "TCP",
listenFrom: "server"
};
}
return null;
},
}
});
}
throw new Error("install server or check if server installed correctly!");
}

View File

@ -0,0 +1,12 @@
import path from "node:path";
import { listVersion, Bedrock } from "../src/platform/bedrock/index.js";
import { homedir } from "node:os";
const server = new Bedrock(path.join(homedir(), ".bdsmaneger/playgroud/mojang"), "pocketmine");
server.once("installedVersion", version => console.log("Installed %s server", version));
await listVersion.listPocketmineProject();
console.log("Init install");
await server.installServer(0);
server.on("logLine", (line) => console.log(line[0]));
const pr = await server.runServer();
process.stdin.pipe(pr.stdin);

View File

@ -0,0 +1,11 @@
import path from "node:path";
import { Java } from "../src/platform/java/index.js";
import { homedir } from "node:os";
import { rm } from "node:fs/promises";
await rm(path.join(homedir(), ".bdsmaneger/playgroud/java"), { recursive: true }).catch(() => {});
const server = new Java(path.join(homedir(), ".bdsmaneger/playgroud/java"), "cuberite");
await server.installServer(0);
server.on("logLine", line => console.log(line));
const run = await server.runServer();
process.stdin.pipe(run.stdin);

View File

@ -0,0 +1,7 @@
# Node
node_modules/
# Typescript
**/*.js
**/*.d.ts
**/*.tsbuildinfo

View File

@ -1,133 +1,186 @@
#!/usr/bin/env node #!/usr/bin/env node
import { Java, Bedrock } from "@the-bds-maneger/core"; import { Bedrock, Java } from "@the-bds-maneger/core";
import { createServer } from "node:http";
import express from "express"; import express from "express";
import expressLayer from "express/lib/router/layer.js"; import expressLayer from "express/lib/router/layer.js";
import { createServer } from "node:http";
import { format } from "node:util";
process.on("unhandledRejection", err => console.error(err));
expressLayer.prototype.handle_request = async function handle_request_promised(...args) { expressLayer.prototype.handle_request = async function handle_request_promised(...args) {
var fn = this.handle; var fn = this.handle;
if (fn.length > 3) return args.at(-1)(); if (fn.length > 3) return args.at(-1)();
await Promise.resolve().then(() => fn.call(this, ...args)).catch(args.at(-1)); await Promise.resolve().then(() => fn.call(this, ...args)).catch(args.at(-1));
} }
/**
* @param {number} nextTime
*/
function printDate(nextTime = 0) {
const dd = new Date(Date.now() + nextTime);
return format("%f/%f/%f %f:%f:%f", dd.getDate(), dd.getMonth() + 1, dd.getFullYear(), dd.getMinutes(), dd.getHours(), dd.getSeconds());
}
const interval = 1000 * 60 * 60 * 2;
console.log("Initial versions");
await Promise.all([Bedrock.listVersion.syncCaches(), Java.listVersion.syncCaches()]);
console.log("Next sync in", printDate(interval));
setInterval(async () => {
console.log("Sync versions");
await Promise.all([Bedrock.listVersion.syncCaches(), Java.listVersion.syncCaches()]);
console.log("Next sync in", printDate(interval));
}, interval);
const app = express(); const app = express();
app.use((req, res, next) => {
if (!(req.query.pretty === "off" || req.query.pretty === "false")) {
res.json = (body) => res.setHeader("Content-Type", "application/json").send(JSON.stringify(body, null, 2));
}
next();
});
const server = createServer(app); const server = createServer(app);
server.listen(Number(process.env.PORT || 3000), () => { server.listen(Number(process.env.PORT || 3000), () => {
const addr = server.address(); const addr = server.address();
console.log("Listening on http://localhost:%f", Number(typeof addr === "object" ? addr.port : addr)); if (typeof addr === "string") console.log("Server listen on socket %O path", addr);
else if (typeof addr === "number") console.log("Listening on http://localhost:%f", addr);
else if (typeof addr === "object") console.log("Listen on http://localhost:%s", addr?.port);
}); });
// Bedrock // Bedrock
const bedrockRoute = express.Router(); const bedrockRoute = express.Router();
app.use("/bedrock", bedrockRoute); app.use("/bedrock", bedrockRoute);
const bedrockStash = {}; bedrockRoute.get("/", async ({ res }) => {
bedrockRoute.get("/", async ({res}) => {
const bedrockOficial = (await (async () => {if (bedrockStash["oficial"]) return bedrockStash["oficial"];bedrockStash["oficial"] = await Bedrock.listVersions();setTimeout(() => delete bedrockStash["oficial"], 60000);return bedrockStash["oficial"];})()).filter(rel => rel.release === "stable").at(0);
const cloudbust = (await (async () => {if (bedrockStash["cloudbust"]) return bedrockStash["cloudbust"];bedrockStash["cloudbust"] = await Bedrock.listVersions("cloudbust");setTimeout(() => delete bedrockStash["cloudbust"], 60000);return bedrockStash["cloudbust"];})()).at(0);
const nukkit = (await (async () => {if (bedrockStash["nukkit"]) return bedrockStash["nukkit"];bedrockStash["nukkit"] = await Bedrock.listVersions("nukkit");setTimeout(() => delete bedrockStash["nukkit"], 60000);return bedrockStash["nukkit"];})()).at(0);
const powernukkit = (await (async () => {if (bedrockStash["powernukkit"]) return bedrockStash["powernukkit"];bedrockStash["powernukkit"] = await Bedrock.listVersions("powernukkit");setTimeout(() => delete bedrockStash["powernukkit"], 60000);return bedrockStash["powernukkit"];})()).at(0);
const pocketmine = (await (async () => {if (bedrockStash["pocketmine"]) return bedrockStash["pocketmine"];bedrockStash["pocketmine"] = await Bedrock.listVersions("pocketmine");setTimeout(() => delete bedrockStash["pocketmine"], 60000);return bedrockStash["pocketmine"];})()).filter(rel => rel.release === "stable").at(0);
return res.json({ return res.json({
bedrockOficial, bedrockOficial: Bedrock.listVersion.mojangCache,
pocketmine, pocketmine: Bedrock.listVersion.pocketmineCache,
cloudbust, cloudbust: Bedrock.listVersion.cloudburstCache,
nukkit, nukkit: Bedrock.listVersion.nukkitCache,
powernukkit powernukkit: Bedrock.listVersion.powernukkitCache
}); });
}); });
bedrockRoute.get("/((oficial|cloudbust|nukkit|nukkit|powernukkit|pocketmine))", async (req, res) => { bedrockRoute.get("/((oficial|cloudbust|nukkit|nukkit|powernukkit|pocketmine))", async (req, res) => {
let platform = req.params[0]; /** @type {"oficial" | "cloudbust" | "nukkit" | "nukkit" | "powernukkit" | "pocketmine"} */
if (!bedrockStash[platform]) { const platform = req.params[0];
if (platform === "oficial") platform = null;
bedrockStash[(platform === null ? "oficial" : platform)] = await Bedrock.listVersions(platform);
setTimeout(() => delete bedrockStash[(platform === null ? "oficial" : platform)], 60000);
}
const list = bedrockStash[(platform === null ? "oficial" : platform)];
const ver = String(req.query.v || req.query.version || ""); const ver = String(req.query.v || req.query.version || "");
if (ver) { if (platform === "oficial") {
const data = list.find(e => e.version === ver); if (!ver) return res.json(Bedrock.listVersion.mojangCache.toJSON());
if (!data) return res.status(400).json({error: "Not found version."}); else {
return res.json(data); if (!(Bedrock.listVersion.mojangCache.has(ver))) return res.status(400).json({error: "This version not exists!"});
return res.json(Bedrock.listVersion.mojangCache.get(ver));
}
} else if (platform === "pocketmine") {
if (!ver) return res.json(Bedrock.listVersion.pocketmineCache.toJSON());
else {
if (!(Bedrock.listVersion.pocketmineCache.has(ver))) return res.status(400).json({error: "This version not exists!"});
return res.json(Bedrock.listVersion.pocketmineCache.get(ver));
}
} else if (platform === "nukkit") {
if (!ver) return res.json(Bedrock.listVersion.nukkitCache.toJSON());
else {
if (!(Bedrock.listVersion.nukkitCache.has(ver))) return res.status(400).json({error: "This version not exists!"});
return res.json(Bedrock.listVersion.nukkitCache.get(ver));
}
} else if (platform === "cloudbust") {
if (!ver) return res.json(Bedrock.listVersion.cloudburstCache.toJSON());
else {
if (!(Bedrock.listVersion.cloudburstCache.has(ver))) return res.status(400).json({error: "This version not exists!"});
return res.json(Bedrock.listVersion.cloudburstCache.get(ver));
}
} else if (platform === "powernukkit") {
if (!ver) return res.json(Bedrock.listVersion.powernukkitCache.toJSON());
else {
if (!(Bedrock.listVersion.powernukkitCache.has(ver))) return res.status(400).json({error: "This version not exists!"});
return res.json(Bedrock.listVersion.powernukkitCache.get(ver));
}
} }
return res.json(list);
}); });
// Java // Java
const javaRoute = express.Router(); const javaRoute = express.Router();
app.use("/java", javaRoute); app.use("/java", javaRoute);
const javaStash = {}; javaRoute.get("/", async ({ res }) => {
javaRoute.get("/", async ({res}) => {
const javaOficial = (await (async () => {if (javaStash["oficial"]) return javaStash["oficial"]; javaStash["oficial"] = await Java.listVersions(); setTimeout(() => delete javaStash["oficial"], 6000); return javaStash["oficial"];})()).filter(rel => rel.release === "stable").at(0);
const spigot = (await (async () => {if (javaStash["spigot"]) return javaStash["spigot"]; javaStash["spigot"] = await Java.listVersions("spigot"); setTimeout(() => delete javaStash["spigot"], 6000); return javaStash["spigot"];})()).at(0);
const paper = (await (async () => {if (javaStash["paper"]) return javaStash["paper"]; javaStash["paper"] = await Java.listVersions("paper"); setTimeout(() => delete javaStash["paper"], 6000); return javaStash["paper"];})()).at(0);
const glowstone = (await (async () => {if (javaStash["glowstone"]) return javaStash["glowstone"]; javaStash["glowstone"] = await Java.listVersions("glowstone"); setTimeout(() => delete javaStash["glowstone"], 6000); return javaStash["glowstone"];})()).at(0);
const purpur = (await (async () => {if (javaStash["purpur"]) return javaStash["purpur"]; javaStash["purpur"] = await Java.listVersions("purpur"); setTimeout(() => delete javaStash["purpur"], 6000); return javaStash["purpur"];})()).at(0);
const folia = (await (async () => {if (javaStash["folia"]) return javaStash["folia"]; javaStash["folia"] = await Java.listVersions("folia"); setTimeout(() => delete javaStash["folia"], 6000); return javaStash["folia"];})()).at(0);
const cuberite = (await (async () => {if (javaStash["cuberite"]) return javaStash["cuberite"]; javaStash["cuberite"] = await Java.listVersions("cuberite"); setTimeout(() => delete javaStash["cuberite"], 6000); return javaStash["cuberite"];})()).at(0);
return res.json({ return res.json({
javaOficial, javaOficial: Java.listVersion.mojangCache,
spigot, spigot: Java.listVersion.spigotCache,
paper, paper: Java.listVersion.paperCache,
glowstone, glowstone: Java.listVersion.glowstoneCache,
purpur, purpur: Java.listVersion.purpurCache,
folia, folia: Java.listVersion.foliaCache,
cuberite cuberite: Java.listVersion.cuberiteCache
}); });
}); });
javaRoute.get("/((oficial|spigot|paper|purpur|glowstone|folia|cuberite))", async (req, res) => { javaRoute.get("/((oficial|spigot|paper|purpur|glowstone|folia|cuberite))", async (req, res) => {
let platform = req.params[0]; /** @type {"oficial" | "spigot" | "paper" | "purpur" | "glowstone" | "folia" | "cuberite"} */
if (!javaStash[platform]) { const platform = req.params[0];
if (platform === "oficial") platform = null;
javaStash[(platform === null ? "oficial" : platform)] = await Java.listVersions(platform);
setTimeout(() => delete javaStash[(platform === null ? "oficial" : platform)], 60000);
}
const list = javaStash[(platform === null ? "oficial" : platform)];
const ver = String(req.query.v || req.query.version || ""); const ver = String(req.query.v || req.query.version || "");
if (ver) { if (platform === "oficial") {
const data = list.find(e => e.version === ver); if (!ver) return res.json(Java.listVersion.mojangCache.toJSON());
if (!data) return res.status(400).json({error: "Not found version."}); else {
return res.json(data); if (!(Java.listVersion.mojangCache.has(ver))) return res.status(400).json({error: "This version not exists!"});
return res.json(Java.listVersion.mojangCache.get(ver));
}
} else if (platform === "spigot") {
if (!ver) return res.json(Java.listVersion.spigotCache.toJSON());
else {
if (!(Java.listVersion.spigotCache.has(ver))) return res.status(400).json({error: "This version not exists!"});
return res.json(Java.listVersion.spigotCache.get(ver));
}
} else if (platform === "paper") {
if (!ver) return res.json(Java.listVersion.paperCache.toJSON());
else {
if (!(Java.listVersion.paperCache.has(ver))) return res.status(400).json({error: "This version not exists!"});
return res.json(Java.listVersion.paperCache.get(ver));
}
} else if (platform === "purpur") {
if (!ver) return res.json(Java.listVersion.purpurCache.toJSON());
else {
if (!(Java.listVersion.purpurCache.has(ver))) return res.status(400).json({error: "This version not exists!"});
return res.json(Java.listVersion.purpurCache.get(ver));
}
} else if (platform === "glowstone") {
if (!ver) return res.json(Java.listVersion.glowstoneCache.toJSON());
else {
if (!(Java.listVersion.glowstoneCache.has(ver))) return res.status(400).json({error: "This version not exists!"});
return res.json(Java.listVersion.glowstoneCache.get(ver));
}
} else if (platform === "cuberite") {
if (!ver) return res.json(Java.listVersion.cuberiteCache.toJSON());
else {
if (!(Java.listVersion.cuberiteCache.has(ver))) return res.status(400).json({error: "This version not exists!"});
return res.json(Java.listVersion.cuberiteCache.get(ver));
}
} else if (platform === "folia") {
if (!ver) return res.json(Java.listVersion.foliaCache.toJSON());
else {
if (!(Java.listVersion.foliaCache.has(ver))) return res.status(400).json({error: "This version not exists!"});
return res.json(Java.listVersion.foliaCache.get(ver));
}
} }
return res.json(list);
}); });
app.get("/", async ({res}) => { app.get("/", async ({ res }) => {
// Bedrock
const bedrockOficial = (await (async () => {if (bedrockStash["oficial"]) return bedrockStash["oficial"];bedrockStash["oficial"] = await Bedrock.listVersions();setTimeout(() => delete bedrockStash["oficial"], 60000);return bedrockStash["oficial"];})()).filter(rel => rel.release === "stable").at(0);
const cloudbust = (await (async () => {if (bedrockStash["cloudbust"]) return bedrockStash["cloudbust"];bedrockStash["cloudbust"] = await Bedrock.listVersions("cloudbust");setTimeout(() => delete bedrockStash["cloudbust"], 60000);return bedrockStash["cloudbust"];})()).at(0);
const nukkit = (await (async () => {if (bedrockStash["nukkit"]) return bedrockStash["nukkit"];bedrockStash["nukkit"] = await Bedrock.listVersions("nukkit");setTimeout(() => delete bedrockStash["nukkit"], 60000);return bedrockStash["nukkit"];})()).at(0);
const powernukkit = (await (async () => {if (bedrockStash["powernukkit"]) return bedrockStash["powernukkit"];bedrockStash["powernukkit"] = await Bedrock.listVersions("powernukkit");setTimeout(() => delete bedrockStash["powernukkit"], 60000);return bedrockStash["powernukkit"];})()).at(0);
const pocketmine = (await (async () => {if (bedrockStash["pocketmine"]) return bedrockStash["pocketmine"];bedrockStash["pocketmine"] = await Bedrock.listVersions("pocketmine");setTimeout(() => delete bedrockStash["pocketmine"], 60000);return bedrockStash["pocketmine"];})()).filter(rel => rel.release === "stable").at(0);
// Java
const javaOficial = (await (async () => {if (javaStash["oficial"]) return javaStash["oficial"]; javaStash["oficial"] = await Java.listVersions(); setTimeout(() => delete javaStash["oficial"], 6000); return javaStash["oficial"];})()).filter(rel => rel.release === "stable").at(0);
const spigot = (await (async () => {if (javaStash["spigot"]) return javaStash["spigot"]; javaStash["spigot"] = await Java.listVersions("spigot"); setTimeout(() => delete javaStash["spigot"], 6000); return javaStash["spigot"];})()).at(0);
const paper = (await (async () => {if (javaStash["paper"]) return javaStash["paper"]; javaStash["paper"] = await Java.listVersions("paper"); setTimeout(() => delete javaStash["paper"], 6000); return javaStash["paper"];})()).at(0);
const glowstone = (await (async () => {if (javaStash["glowstone"]) return javaStash["glowstone"]; javaStash["glowstone"] = await Java.listVersions("glowstone"); setTimeout(() => delete javaStash["glowstone"], 6000); return javaStash["glowstone"];})()).at(0);
const purpur = (await (async () => {if (javaStash["purpur"]) return javaStash["purpur"]; javaStash["purpur"] = await Java.listVersions("purpur"); setTimeout(() => delete javaStash["purpur"], 6000); return javaStash["purpur"];})()).at(0);
const folia = (await (async () => {if (javaStash["folia"]) return javaStash["folia"]; javaStash["folia"] = await Java.listVersions("folia"); setTimeout(() => delete javaStash["folia"], 6000); return javaStash["folia"];})()).at(0);
const cuberite = (await (async () => {if (javaStash["cuberite"]) return javaStash["cuberite"]; javaStash["cuberite"] = await Java.listVersions("cuberite"); setTimeout(() => delete javaStash["cuberite"], 6000); return javaStash["cuberite"];})()).at(0);
return res.json({ return res.json({
bedrock: { bedrock: {
bedrockOficial, bedrockOficial: Bedrock.listVersion.mojangCache,
pocketmine, pocketmine: Bedrock.listVersion.pocketmineCache,
cloudbust, cloudbust: Bedrock.listVersion.cloudburstCache,
nukkit, nukkit: Bedrock.listVersion.nukkitCache,
powernukkit powernukkit: Bedrock.listVersion.powernukkitCache
}, },
java: { java: {
javaOficial, javaOficial: Java.listVersion.mojangCache,
spigot, spigot: Java.listVersion.spigotCache,
paper, paper: Java.listVersion.paperCache,
glowstone, glowstone: Java.listVersion.glowstoneCache,
purpur, purpur: Java.listVersion.purpurCache,
folia, folia: Java.listVersion.foliaCache,
cuberite cuberite: Java.listVersion.cuberiteCache
} }
}) })
}); });
// 404 // 404
app.use(({res}) => res.status(404).json({error: "Not found page."})); app.use(({ res }) => res.status(404).json({ error: "Not found page." }));
app.use((error, _req, res, _next) => res.status(500).json({error: error?.message || String(error) || "Unknown error."})); app.use((error, _req, res, _next) => res.status(500).json({ error: error?.message || String(error) || "Unknown error." }));

9
railway.json Normal file
View File

@ -0,0 +1,9 @@
{
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "packages/verapiDockerfile"
},
"deploy": {
"restartPolicyType": "ALWAYS"
}
}