const axios = require("axios"); const fs = require("fs"); const path = require("path"); const { pipeline } = require("stream/promises"); const cliProgress = require("cli-progress"); const cheerio = require("cheerio"); const SftpClient = require("ssh2-sftp-client"); const crypto = require("crypto"); // --- Config & Secrets --- const CONFIG_PATH = "config.json"; const SECRET_PATH = "secret.key"; // Load or create encryption key let secret; if (!fs.existsSync(SECRET_PATH)) { secret = crypto.randomBytes(32); fs.writeFileSync(SECRET_PATH, secret.toString("hex")); console.log("šŸ” Generated and saved a new secret key."); } else { secret = Buffer.from(fs.readFileSync(SECRET_PATH, "utf-8"), "hex"); } function encryptPassword(password) { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv("aes-256-cbc", secret, iv); const encrypted = Buffer.concat([cipher.update(password, "utf8"), cipher.final()]); return { iv: iv.toString("hex"), password: encrypted.toString("hex"), }; } function decryptPassword(encrypted) { const decipher = crypto.createDecipheriv( "aes-256-cbc", secret, Buffer.from(encrypted.iv, "hex") ); const decrypted = Buffer.concat([ decipher.update(Buffer.from(encrypted.password, "hex")), decipher.final(), ]); return decrypted.toString("utf8"); } // Load config and auto-convert plaintext SFTP password let config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")); const sftpConfig = config.sftp || { enabled: false }; if (sftpConfig.password && !sftpConfig.encryptedPassword) { sftpConfig.encryptedPassword = encryptPassword(sftpConfig.password); delete sftpConfig.password; config.sftp = sftpConfig; fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2)); console.log("šŸ” Encrypted SFTP password saved to config.json."); } // --- Utilities --- const ensureDir = (dir) => { if (!fs.existsSync(dir)) fs.mkdirSync(dir); }; const isDownloaded = (filename) => fs.existsSync(path.join("downloads", filename)); const downloadJar = async (url, name) => { if (isDownloaded(name)) { console.log(`🟔 Skipped (already exists): ${name}`); return; } const response = await axios.get(url, { responseType: "stream", headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", }, }); const total = parseInt(response.headers["content-length"] || "0", 10); const bar = new cliProgress.SingleBar({}, cliProgress.Presets.shades_classic); bar.start(total, 0); response.data.on("data", (chunk) => bar.increment(chunk.length)); response.data.on("end", () => bar.stop()); const filePath = path.join("downloads", name); await pipeline(response.data, fs.createWriteStream(filePath)); console.log(`āœ”ļø Saved: ${name}`); }; // --- Source handlers --- const handleJenkins = async (url) => { const html = await axios.get(url).then((res) => res.data); const $ = cheerio.load(html); const links = $("a[href$='.jar']").map((_, el) => $(el).attr("href")).get(); if (!links.length) throw new Error("No .jar files found on page"); const base = new URL(url); const preferred = ["paper", "spigot", "bukkit"]; const skip = ["javadoc", "sources", "cli", "bootstrap", "mojangapi", "nashorn", "remapper"]; const essentialsOK = ["EssentialsX", "EssentialsXChat", "EssentialsXSpawn", "EssentialsXGeoIP"]; const valid = links .map((href) => { const fileName = path.basename(href); const lower = fileName.toLowerCase(); if (skip.some((term) => lower.includes(term))) return null; if (fileName.startsWith("EssentialsX")) { const base = fileName.split("-")[0]; if (!essentialsOK.includes(base)) return null; } return { href, fileName: lower }; }) .filter(Boolean); for (const tag of preferred) { const pick = valid.find((f) => f.fileName.includes(tag)); if (pick) { const finalURL = new URL(pick.href, base).href; await downloadJar(finalURL, path.basename(pick.href)); return; } } // Fallback: all valid jars for (const f of valid) { const finalURL = new URL(f.href, base).href; await downloadJar(finalURL, path.basename(f.href)); } }; const handleGitHub = async (url) => { const match = url.match(/github\.com\/([^/]+\/[^/]+)/); if (!match) throw new Error("Invalid GitHub URL format"); const repo = match[1]; const apiURL = `https://api.github.com/repos/${repo}/releases/latest`; const { data } = await axios.get(apiURL); const preferred = data.assets.find((a) => a.name === "GeyserSkinManager-Spigot.jar"); const fallback = data.assets.find((a) => a.name.endsWith(".jar")); const chosen = preferred || fallback; if (!chosen) throw new Error("No .jar assets found"); await downloadJar(chosen.browser_download_url, chosen.name); }; const handleModrinth = async (url) => { const match = url.match(/modrinth\.com\/plugin\/([^/]+)/); if (!match) throw new Error("Invalid Modrinth URL format"); const project = match[1]; const versions = await axios .get(`https://api.modrinth.com/v2/project/${project}/version`) .then((res) => res.data); const latest = versions[0]; const file = latest.files.find((f) => f.filename.endsWith(".jar")); if (!file) throw new Error("No .jar file in latest version"); await downloadJar(file.url, file.filename); }; const handleDirect = async (url) => { let name = path.basename(url.split("?")[0]); if (url.includes("download.geysermc.org")) { name = "floodgate-spigot.jar"; } else { try { const head = await axios.head(url); const disp = head.headers["content-disposition"]; const match = disp?.match(/filename[^=]*=(?:UTF-8'')?["']?([^"';]+)/i); if (match) name = decodeURIComponent(match[1]); } catch {} } name = name.replace(/[<>:"/\\|?*\x00-\x1F]/g, ""); await downloadJar(url, name); }; // --- Upload to SFTP --- const uploadToSFTP = async () => { if (!sftpConfig.enabled) { console.log("šŸ“¦ SFTP is disabled in config."); return; } const sftp = new SftpClient(); const remote = sftpConfig.remotePath || "/"; const files = fs.readdirSync("downloads").filter((f) => f.endsWith(".jar")); const connectOptions = { host: sftpConfig.host, port: sftpConfig.port || 22, username: sftpConfig.username, }; if (sftpConfig.privateKeyPath && fs.existsSync(sftpConfig.privateKeyPath)) { connectOptions.privateKey = fs.readFileSync(sftpConfig.privateKeyPath); } else if (sftpConfig.encryptedPassword) { connectOptions.password = decryptPassword(sftpConfig.encryptedPassword); } else { throw new Error("Missing SFTP password or private key."); } try { await sftp.connect(connectOptions); for (const file of files) { const local = path.join("downloads", file); const remoteFile = path.join(remote, file).replace(/\\/g, "/"); console.log(`šŸš€ Uploading ${file} → ${remoteFile}`); await sftp.put(local, remoteFile); } await sftp.end(); console.log("āœ… SFTP upload finished."); } catch (err) { console.error("āŒ SFTP upload failed:", err.message); } }; // --- Main --- (async () => { ensureDir("downloads"); for (const url of config.urls) { console.log(`\nšŸ“„ ${url}`); try { if (url.includes("github.com")) { await handleGitHub(url); } else if (url.includes("modrinth.com")) { await handleModrinth(url); } else if (url.includes("/lastSuccessfulBuild/")) { await handleJenkins(url); } else if (url.endsWith(".jar") || url.includes("download.geysermc.org")) { await handleDirect(url); } else { console.warn("āš ļø Skipping unknown URL format."); } } catch (err) { console.error(`āŒ Failed: ${err.message}`); } } await uploadToSFTP(); })();