diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 8ccd13c..0000000 --- a/.dockerignore +++ /dev/null @@ -1,14 +0,0 @@ -# npm -/*.tgz - -# Node -node_modules/ - -# Typescript -**/*.js -**/*.d.ts -**/tsconfig.tsbuildinfo - -# PHP and Spigot Pre builds -phpOutput/ -*.jar \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 0c5f35e..627b1eb 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -38,7 +38,7 @@ jobs: # Build Core - name: Core Build 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 }}" run: npm run --if-present -w "${{ matrix.package }}" prepack \ No newline at end of file diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 4f185d3..11374d4 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -5,91 +5,10 @@ import yargs from "yargs"; // Init yargs yargs(process.argv.slice(2)).version(false).help(true).strictCommands().demandCommand().alias("h", "help") -.command("install", "Install server", async yargs => { - const options = 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"], - }) - .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(["install", "i", "update"], "Install/update server platform", yargs => yargs, async options => { + console.log(bdsCore); }) +.command(["start", "run", "$0"], "start server", yargs => yargs, async options => {}) .parseAsync().catch(err => { console.log(err); process.exit(1); diff --git a/packages/core/README.md b/packages/core/README.md index 6551eae..dde1be0 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,37 +1,3 @@ # Bds Maneger Core -Basic core to install, update and manage several minecraft servers automatically, depending on a few dependencies, the basic being **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. \ No newline at end of file +Um simples nucleo com exposição de APIs para integração com os Servidores de Minecraft para gerenciamento no Nodejs. \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 6bb2c09..9d366f5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@the-bds-maneger/core", "version": "6.0.4", - "description": "", + "description": "Core APIs to maneger Minecraft servers platforms", "main": "src/index.js", "types": "src/index.d.ts", "type": "module", @@ -11,24 +11,22 @@ "access": "public" }, "scripts": { - "prepack": "tsc --build --clean && tsc", - "postpack": "tsc --build --clean", - "build": "tsc --build --clean && tsc" + "prepack": "tsc --build --clean && tsc --build", + "postpack": "tsc --build --clean" }, "dependencies": { "@sirherobrine23/cloud": "^3.6.11", "@sirherobrine23/extends": "^3.6.11", "@sirherobrine23/http": "^3.6.11", + "adm-zip": "0.5.10", "sanitize-filename": "^1.6.3", "semver": "^7.5.1", "tar": "^6.1.15", - "unzip-stream": "^0.3.1", - "unzipper": "^0.10.14" + "xml-js": "^1.6.11" }, "devDependencies": { + "@types/adm-zip": "0.5.0", "@types/semver": "^7.5.0", - "@types/tar": "^6.1.5", - "@types/unzip-stream": "^0.3.1", - "@types/unzipper": "^0.10.6" + "@types/tar": "^6.1.5" } } diff --git a/packages/core/src/internal.ts b/packages/core/src/internal.ts deleted file mode 100644 index 79e428f..0000000 --- a/packages/core/src/internal.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { oracleBucket } from "@sirherobrine23/cloud"; -/** - * Bucket readonly - */ -export const oracleStorage = oracleBucket.oracleBucketPreAuth("sa-saopaulo-1", "grwodtg32n4d", "bdsFiles", "0IKM-5KFpAF8PuWoVe86QFsF4sipU2rXfojpaOMEdf4QgFQLcLlDWgMSPHWmjf5W"); \ No newline at end of file diff --git a/packages/core/src/internalClouds.ts b/packages/core/src/internalClouds.ts new file mode 100644 index 0000000..e3066a6 --- /dev/null +++ b/packages/core/src/internalClouds.ts @@ -0,0 +1,5 @@ +import { oracleBucket } from "@sirherobrine23/cloud"; +/** + * Bucket readonly + */ +export const bdsFilesBucket = oracleBucket.oracleBucketPreAuth("sa-saopaulo-1", "grwodtg32n4d", "bdsFiles", "0IKM-5KFpAF8PuWoVe86QFsF4sipU2rXfojpaOMEdf4QgFQLcLlDWgMSPHWmjf5W"); \ No newline at end of file diff --git a/packages/core/src/platform/bedrock/index.ts b/packages/core/src/platform/bedrock/index.ts new file mode 100644 index 0000000..f0ef56b --- /dev/null +++ b/packages/core/src/platform/bedrock/index.ts @@ -0,0 +1,2 @@ +export * as listVersion from "./listVersion.js"; +export * from "./main.js"; \ No newline at end of file diff --git a/packages/core/src/platform/bedrock/listVersion.ts b/packages/core/src/platform/bedrock/listVersion.ts new file mode 100644 index 0000000..473d286 --- /dev/null +++ b/packages/core/src/platform/bedrock/listVersion.ts @@ -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(); +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(); +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(); +export const cloudburstCache = new versionsStorages(); +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(); +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() + ]); +} \ No newline at end of file diff --git a/packages/core/src/platform/bedrock/main.ts b/packages/core/src/platform/bedrock/main.ts new file mode 100644 index 0000000..7ca447a --- /dev/null +++ b/packages/core/src/platform/bedrock/main.ts @@ -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 { + constructor(origem?: Record) { + 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): event is Bedrock { + return event instanceof Bedrock; +} + +export type bedrockEvents = defineEvents<{ + logLine(lineString: string): void; + portListen(info: bedrockPorts): void; + installedVersion(serverVersion: string): void; +}>; + +export class Bedrock

extends customEvent { + 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((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"); + } + } +} \ No newline at end of file diff --git a/packages/core/src/platform/java/index.ts b/packages/core/src/platform/java/index.ts new file mode 100644 index 0000000..f0ef56b --- /dev/null +++ b/packages/core/src/platform/java/index.ts @@ -0,0 +1,2 @@ +export * as listVersion from "./listVersion.js"; +export * from "./main.js"; \ No newline at end of file diff --git a/packages/core/src/platform/java/listVersion.ts b/packages/core/src/platform/java/listVersion.ts new file mode 100644 index 0000000..981650d --- /dev/null +++ b/packages/core/src/platform/java/listVersion.ts @@ -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(arrayInput: T, fn: (value: T[number]) => any): Promise>[]> { + 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>[] = []; + 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(); +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; + craftbukkit?(): Promise; +} + +export const spigotCache = new versionsStorages(); +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(); +export const velocityCache = new versionsStorages(); +export const foliaCache = new versionsStorages(); +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(); +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(); +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(), + ]); +} \ No newline at end of file diff --git a/packages/core/src/platform/java/main.ts b/packages/core/src/platform/java/main.ts new file mode 100644 index 0000000..a608450 --- /dev/null +++ b/packages/core/src/platform/java/main.ts @@ -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): event is Java { + return event instanceof Java; +} + +export class Java

extends customEvent { + 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((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; + } +} \ No newline at end of file diff --git a/packages/core/src/reindex.ts b/packages/core/src/reindex.ts index 7541662..9a5ae24 100644 --- a/packages/core/src/reindex.ts +++ b/packages/core/src/reindex.ts @@ -1,4 +1,9 @@ -export * from "./serverManeger.js"; -export * as serverManeger from "./serverManeger.js"; -export * as Bedrock from "./servers/bedrock.js"; -export * as Java from "./servers/java.js"; \ No newline at end of file +// Bedrock platform +export * as Bedrock from "./platform/bedrock/index.js"; +export * as bedrock from "./platform/bedrock/index.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"; \ No newline at end of file diff --git a/packages/core/src/serverManeger.ts b/packages/core/src/serverManeger.ts deleted file mode 100644 index 537587a..0000000 --- a/packages/core/src/serverManeger.ts +++ /dev/null @@ -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|Promise; - -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): ReturnType -}; - -/** - * Platform path maneger - */ -export async function serverManeger(platform: serverManegerV1["platform"], options: manegerOptions): Promise { - 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) { - return runServer({...options, cwd: serverFolder}); - } - }; -} - -export async function listIDs(): Promise<{id: string, platform: "bedrock"|"java", delete: () => Promise}[]> { - 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, - playerAction?(this: serverRun, lineString: string): withPromise, - hotBackup?(this: serverRun): withPromise, - portListen?(this: serverRun, lineString: string): withPromise, - onAvaible?(this: serverRun, lineString: string): withPromise, - postStart?: ((this: serverRun) => withPromise)[], - } -}; - -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>>; -} - -/** - * Run servers globally and hormonally across servers - */ -export async function runServer(options: runOptions): Promise { - 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); - 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); - 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).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); - 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; -} diff --git a/packages/core/src/serverRun.ts b/packages/core/src/serverRun.ts new file mode 100644 index 0000000..19e19c3 --- /dev/null +++ b/packages/core/src/serverRun.ts @@ -0,0 +1,70 @@ +import EventEmitter from "node:events"; + +export type EventMap = Record void>; +export type defineEvents = T; +type EventKey = string & keyof T; + +export interface customEvent extends EventEmitter { + emit>(eventName: K, ...args: Parameters): boolean; + emit(name: "error", err: Error): boolean; + + on>(eventName: K, fn: T[K]): this; + on(eventName: "error", fn: (err: Error) => void): this; + + prependListener>(eventName: K, fn: T[K]): this; + prependListener(eventName: "error", fn: (err: Error) => void): this; + + once>(eventName: K, fn: T[K]): this; + once(eventName: "error", fn: (err: Error) => void): this; + + prependOnceListener>(eventName: K, fn: T[K]): this; + prependOnceListener(eventName: "error", fn: (err: Error) => void): this; + + removeListener>(eventName: K, fn: T[K]): this; + removeListener(eventName: "error", fn: (err: Error) => void): this; + + off>(eventName: K, fn: T[K]): this; + off(eventName: "error", fn: (err: Error) => void): this; + + removeAllListeners>(eventName: K): this; + removeAllListeners(eventName: "error"): this; + + rawListeners>(eventName: K): (T[K])[]; + rawListeners(eventName: "error"): ((err: Error) => void)[]; + + eventNames(): (EventKey | "error")[]; +} + +export class customEvent extends EventEmitter { + constructor() { + super({captureRejections: true}); + } +}; + +export class versionsStorages extends Map { + constructor(origem: Record = {}) { + 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; + }, {}); + } +} \ No newline at end of file diff --git a/packages/core/src/servers/bedrock.ts b/packages/core/src/servers/bedrock.ts deleted file mode 100644 index eb92231..0000000 --- a/packages/core/src/servers/bedrock.ts +++ /dev/null @@ -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, - }, - server: { - getServer(): Promise, - 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 { - 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 = { - 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); -} \ No newline at end of file diff --git a/packages/core/src/servers/java.ts b/packages/core/src/servers/java.ts deleted file mode 100644 index 3f6eb04..0000000 --- a/packages/core/src/servers/java.ts +++ /dev/null @@ -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; - fileURL?: string; - }[], -}; - -export async function listVersions(altServer?: javaOptions["altServer"]): Promise { - 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 => ({ - 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 => { - 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 => { - 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!"); -} diff --git a/packages/core/tests/bedrock.ts b/packages/core/tests/bedrock.ts new file mode 100644 index 0000000..7a2d257 --- /dev/null +++ b/packages/core/tests/bedrock.ts @@ -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); \ No newline at end of file diff --git a/packages/core/tests/java.ts b/packages/core/tests/java.ts new file mode 100644 index 0000000..0030be0 --- /dev/null +++ b/packages/core/tests/java.ts @@ -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); diff --git a/packages/verapi/.dockerignore b/packages/verapi/.dockerignore new file mode 100644 index 0000000..051b6af --- /dev/null +++ b/packages/verapi/.dockerignore @@ -0,0 +1,7 @@ +# Node +node_modules/ + +# Typescript +**/*.js +**/*.d.ts +**/*.tsbuildinfo \ No newline at end of file diff --git a/packages/verapi/src/index.js b/packages/verapi/src/index.js index a5b9f4e..d40d984 100644 --- a/packages/verapi/src/index.js +++ b/packages/verapi/src/index.js @@ -1,133 +1,186 @@ #!/usr/bin/env node -import { Java, Bedrock } from "@the-bds-maneger/core"; -import { createServer } from "node:http"; +import { Bedrock, Java } from "@the-bds-maneger/core"; import express from "express"; 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) { var fn = this.handle; if (fn.length > 3) return 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(); +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); server.listen(Number(process.env.PORT || 3000), () => { 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 const bedrockRoute = express.Router(); app.use("/bedrock", bedrockRoute); -const bedrockStash = {}; -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); - +bedrockRoute.get("/", async ({ res }) => { return res.json({ - bedrockOficial, - pocketmine, - cloudbust, - nukkit, - powernukkit + bedrockOficial: Bedrock.listVersion.mojangCache, + pocketmine: Bedrock.listVersion.pocketmineCache, + cloudbust: Bedrock.listVersion.cloudburstCache, + nukkit: Bedrock.listVersion.nukkitCache, + powernukkit: Bedrock.listVersion.powernukkitCache }); }); + bedrockRoute.get("/((oficial|cloudbust|nukkit|nukkit|powernukkit|pocketmine))", async (req, res) => { - let platform = req.params[0]; - if (!bedrockStash[platform]) { - 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)]; + /** @type {"oficial" | "cloudbust" | "nukkit" | "nukkit" | "powernukkit" | "pocketmine"} */ + const platform = req.params[0]; const ver = String(req.query.v || req.query.version || ""); - if (ver) { - const data = list.find(e => e.version === ver); - if (!data) return res.status(400).json({error: "Not found version."}); - return res.json(data); + if (platform === "oficial") { + if (!ver) return res.json(Bedrock.listVersion.mojangCache.toJSON()); + else { + 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 const javaRoute = express.Router(); app.use("/java", javaRoute); -const javaStash = {}; -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); - +javaRoute.get("/", async ({ res }) => { return res.json({ - javaOficial, - spigot, - paper, - glowstone, - purpur, - folia, - cuberite + javaOficial: Java.listVersion.mojangCache, + spigot: Java.listVersion.spigotCache, + paper: Java.listVersion.paperCache, + glowstone: Java.listVersion.glowstoneCache, + purpur: Java.listVersion.purpurCache, + folia: Java.listVersion.foliaCache, + cuberite: Java.listVersion.cuberiteCache }); }); javaRoute.get("/((oficial|spigot|paper|purpur|glowstone|folia|cuberite))", async (req, res) => { - let platform = req.params[0]; - if (!javaStash[platform]) { - 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)]; + /** @type {"oficial" | "spigot" | "paper" | "purpur" | "glowstone" | "folia" | "cuberite"} */ + const platform = req.params[0]; const ver = String(req.query.v || req.query.version || ""); - if (ver) { - const data = list.find(e => e.version === ver); - if (!data) return res.status(400).json({error: "Not found version."}); - return res.json(data); + if (platform === "oficial") { + if (!ver) return res.json(Java.listVersion.mojangCache.toJSON()); + else { + 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}) => { - // 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); - +app.get("/", async ({ res }) => { return res.json({ bedrock: { - bedrockOficial, - pocketmine, - cloudbust, - nukkit, - powernukkit + bedrockOficial: Bedrock.listVersion.mojangCache, + pocketmine: Bedrock.listVersion.pocketmineCache, + cloudbust: Bedrock.listVersion.cloudburstCache, + nukkit: Bedrock.listVersion.nukkitCache, + powernukkit: Bedrock.listVersion.powernukkitCache }, java: { - javaOficial, - spigot, - paper, - glowstone, - purpur, - folia, - cuberite + javaOficial: Java.listVersion.mojangCache, + spigot: Java.listVersion.spigotCache, + paper: Java.listVersion.paperCache, + glowstone: Java.listVersion.glowstoneCache, + purpur: Java.listVersion.purpurCache, + folia: Java.listVersion.foliaCache, + cuberite: Java.listVersion.cuberiteCache } }) }); // 404 -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."})); \ No newline at end of file +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." })); \ No newline at end of file diff --git a/railway.json b/railway.json new file mode 100644 index 0000000..2c4f29f --- /dev/null +++ b/railway.json @@ -0,0 +1,9 @@ +{ + "build": { + "builder": "DOCKERFILE", + "dockerfilePath": "packages/verapiDockerfile" + }, + "deploy": { + "restartPolicyType": "ALWAYS" + } +} \ No newline at end of file