From cc3dfce7dfbbeda9934892be631decb79a26dd28 Mon Sep 17 00:00:00 2001 From: hachem Date: Sun, 12 Oct 2025 15:45:04 +0200 Subject: [add]: updater --- .gitignore | 1 + bot/app.js | 161 +++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- src/app.js | 161 ------------------------------------------------- updater/updater.py | 171 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 334 insertions(+), 162 deletions(-) create mode 100644 bot/app.js delete mode 100644 src/app.js create mode 100644 updater/updater.py diff --git a/.gitignore b/.gitignore index 26dfd2a..0b238f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ config/api.json cache/ +server/ # Logs logs diff --git a/bot/app.js b/bot/app.js new file mode 100644 index 0000000..d25b3b6 --- /dev/null +++ b/bot/app.js @@ -0,0 +1,161 @@ +const { Client, Events, GatewayIntentBits } = require("discord.js") +const axios = require("axios") +const fs = require("fs-extra") +const path = require("path") +const config = require("../config/config.json") +const api = require("../config/api.json") + +const { channel_id, minecraft_version, modrinth_mods } = config +const { token } = api + +const cacheFile = path.join(__dirname, "../cache/mod_versions.json") +const client = new Client({ intents: [GatewayIntentBits.Guilds] }) + +const LoadCache = async () => +{ + try + { + if (await fs.pathExists(cacheFile)) + return JSON.parse(await fs.readFile(cacheFile, "utf8")) + } catch (err) + { + console.error("[Cache] Failed to read cache:", err) + } + return {} +} + +const SaveCache = async (cache) => +{ + try + { + await fs.ensureDir(path.dirname(cacheFile)) + await fs.writeFile(cacheFile, JSON.stringify(cache, null, 2)) + } catch (err) + { + console.error("[Cache] Failed to save cache:", err) + } +} + +const GetCompatibleModVersion = async (slug, targetMinecraftVersion) => +{ + try + { + const res = await axios.get(`https://api.modrinth.com/v2/project/${slug}/version`) + const fabricVersions = res.data.filter(v => v.loaders.includes("fabric")) + if (fabricVersions.length === 0) + return null + + const compatible = fabricVersions.find(v => v.game_versions.includes(targetMinecraftVersion)) + const latest = fabricVersions.sort((a, b) => + new Date(b.date_published) - new Date(a.date_published) + )[0] + + return { latest, compatible: compatible || null } + } catch (err) + { + console.error(`[Modrinth] Error fetching ${slug}:`, err.message) + return null + } +} + +const CheckModUpdates = async () => +{ + console.log("[Check] Checking for mod compatibility...") + const cache = await LoadCache() + const newCache = { ...cache } + const compatibleReleased = [] + const waitingUpdate = [] + const newlyCompatible = [] + + for (const slug of modrinth_mods) + { + const modData = await GetCompatibleModVersion(slug, minecraft_version) + if (!modData) continue + + const { latest, compatible } = modData + const latestId = latest.id + const cachedId = cache[slug] + + if (compatible) + { + compatibleReleased.push({ slug }) + newCache[slug] = latestId + + if (!cachedId || cachedId !== latestId) + { + newlyCompatible.push({ slug }) + console.log(`[Update] ${slug} is now compatible with Minecraft ${minecraft_version}`) + } + } else + { + waitingUpdate.push({ slug }) + console.log(`[Check] ${slug} is waiting for a compatible version`) + } + } + + await SaveCache(newCache) + return { compatibleReleased, waitingUpdate, newlyCompatible } +} + +const SendUpdateMessages = async (channel, compatibleReleased, waitingUpdate, newlyCompatible) => +{ + if (!compatibleReleased.length && !waitingUpdate.length) + { + console.log("[Update]: No mods to report.") + return + } + + let message = `**Mod Compatibility Check — Minecraft ${minecraft_version}**\n\n` + + if (newlyCompatible.length > 0) + { + message += `**🎉 Mods Recently Updated:**\n` + for (const { slug } of newlyCompatible) + message += `✅ ${slug}\n` + message += `\n` + } + + const stillCompatible = compatibleReleased.filter( + m => !newlyCompatible.find(n => n.slug === m.slug) + ) + if (stillCompatible.length > 0) + { + message += `**🟢 Mods Compatible:**\n` + for (const { slug } of stillCompatible) + message += `✅ ${slug}\n` + message += `\n` + } + + if (waitingUpdate.length > 0) + { + message += `**⏳ Mods Waiting on Update:**\n` + for (const { slug } of waitingUpdate) + message += `❌ ${slug}\n` + message += `\n` + } + + await channel.send({ content: message, allowedMentions: { parse: [] }, flags: 4 }) + console.log(`[Discord] Sent update summary (${newlyCompatible.length} updated, ${stillCompatible.length} compatible, ${waitingUpdate.length} waiting).`) +} + +client.once(Events.ClientReady, async readyClient => +{ + console.log(`[OK] Logged in as ${readyClient.user.tag}`) + + const channel = await client.channels.fetch(channel_id).catch(() => null) + if (!channel) { + console.error("❌ Could not find channel ID:", channel_id) + return + } + + const { compatibleReleased, waitingUpdate, newlyCompatible } = await CheckModUpdates() + await SendUpdateMessages(channel, compatibleReleased, waitingUpdate, newlyCompatible) + + setInterval(async () => + { + const { compatibleReleased, waitingUpdate, newlyCompatible } = await CheckModUpdates() + await SendUpdateMessages(channel, compatibleReleased, waitingUpdate, newlyCompatible) + }, 60 * 60 * 1000) +}) + +client.login(token) diff --git a/package.json b/package.json index 4b64e2a..58653f4 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "license": "Apache-2.0", "author": "hachem", "type": "commonjs", - "main": "src/app.js", + "main": "updater/app.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/src/app.js b/src/app.js deleted file mode 100644 index d25b3b6..0000000 --- a/src/app.js +++ /dev/null @@ -1,161 +0,0 @@ -const { Client, Events, GatewayIntentBits } = require("discord.js") -const axios = require("axios") -const fs = require("fs-extra") -const path = require("path") -const config = require("../config/config.json") -const api = require("../config/api.json") - -const { channel_id, minecraft_version, modrinth_mods } = config -const { token } = api - -const cacheFile = path.join(__dirname, "../cache/mod_versions.json") -const client = new Client({ intents: [GatewayIntentBits.Guilds] }) - -const LoadCache = async () => -{ - try - { - if (await fs.pathExists(cacheFile)) - return JSON.parse(await fs.readFile(cacheFile, "utf8")) - } catch (err) - { - console.error("[Cache] Failed to read cache:", err) - } - return {} -} - -const SaveCache = async (cache) => -{ - try - { - await fs.ensureDir(path.dirname(cacheFile)) - await fs.writeFile(cacheFile, JSON.stringify(cache, null, 2)) - } catch (err) - { - console.error("[Cache] Failed to save cache:", err) - } -} - -const GetCompatibleModVersion = async (slug, targetMinecraftVersion) => -{ - try - { - const res = await axios.get(`https://api.modrinth.com/v2/project/${slug}/version`) - const fabricVersions = res.data.filter(v => v.loaders.includes("fabric")) - if (fabricVersions.length === 0) - return null - - const compatible = fabricVersions.find(v => v.game_versions.includes(targetMinecraftVersion)) - const latest = fabricVersions.sort((a, b) => - new Date(b.date_published) - new Date(a.date_published) - )[0] - - return { latest, compatible: compatible || null } - } catch (err) - { - console.error(`[Modrinth] Error fetching ${slug}:`, err.message) - return null - } -} - -const CheckModUpdates = async () => -{ - console.log("[Check] Checking for mod compatibility...") - const cache = await LoadCache() - const newCache = { ...cache } - const compatibleReleased = [] - const waitingUpdate = [] - const newlyCompatible = [] - - for (const slug of modrinth_mods) - { - const modData = await GetCompatibleModVersion(slug, minecraft_version) - if (!modData) continue - - const { latest, compatible } = modData - const latestId = latest.id - const cachedId = cache[slug] - - if (compatible) - { - compatibleReleased.push({ slug }) - newCache[slug] = latestId - - if (!cachedId || cachedId !== latestId) - { - newlyCompatible.push({ slug }) - console.log(`[Update] ${slug} is now compatible with Minecraft ${minecraft_version}`) - } - } else - { - waitingUpdate.push({ slug }) - console.log(`[Check] ${slug} is waiting for a compatible version`) - } - } - - await SaveCache(newCache) - return { compatibleReleased, waitingUpdate, newlyCompatible } -} - -const SendUpdateMessages = async (channel, compatibleReleased, waitingUpdate, newlyCompatible) => -{ - if (!compatibleReleased.length && !waitingUpdate.length) - { - console.log("[Update]: No mods to report.") - return - } - - let message = `**Mod Compatibility Check — Minecraft ${minecraft_version}**\n\n` - - if (newlyCompatible.length > 0) - { - message += `**🎉 Mods Recently Updated:**\n` - for (const { slug } of newlyCompatible) - message += `✅ ${slug}\n` - message += `\n` - } - - const stillCompatible = compatibleReleased.filter( - m => !newlyCompatible.find(n => n.slug === m.slug) - ) - if (stillCompatible.length > 0) - { - message += `**🟢 Mods Compatible:**\n` - for (const { slug } of stillCompatible) - message += `✅ ${slug}\n` - message += `\n` - } - - if (waitingUpdate.length > 0) - { - message += `**⏳ Mods Waiting on Update:**\n` - for (const { slug } of waitingUpdate) - message += `❌ ${slug}\n` - message += `\n` - } - - await channel.send({ content: message, allowedMentions: { parse: [] }, flags: 4 }) - console.log(`[Discord] Sent update summary (${newlyCompatible.length} updated, ${stillCompatible.length} compatible, ${waitingUpdate.length} waiting).`) -} - -client.once(Events.ClientReady, async readyClient => -{ - console.log(`[OK] Logged in as ${readyClient.user.tag}`) - - const channel = await client.channels.fetch(channel_id).catch(() => null) - if (!channel) { - console.error("❌ Could not find channel ID:", channel_id) - return - } - - const { compatibleReleased, waitingUpdate, newlyCompatible } = await CheckModUpdates() - await SendUpdateMessages(channel, compatibleReleased, waitingUpdate, newlyCompatible) - - setInterval(async () => - { - const { compatibleReleased, waitingUpdate, newlyCompatible } = await CheckModUpdates() - await SendUpdateMessages(channel, compatibleReleased, waitingUpdate, newlyCompatible) - }, 60 * 60 * 1000) -}) - -client.login(token) diff --git a/updater/updater.py b/updater/updater.py new file mode 100644 index 0000000..7f065eb --- /dev/null +++ b/updater/updater.py @@ -0,0 +1,171 @@ +from typing import List +import os +import httpx +import shutil +import asyncio +import subprocess +import json + +server_dir = "server" +mods_dir = f"{server_dir}/mods" +os.makedirs(server_dir, exist_ok=True) +os.makedirs(mods_dir, exist_ok=True) + +delete_list = [ + "audioplayer_uploads/", + "config/", + "cache/", + "world/", + "logs/", + + "banned-players.json", + "server.properties", + "banned-ips.json", + "usercache.json", + "whitelist.json", + "ops.json", + "eula.txt", +] + +with open("config/config.json", "r") as f: + config = json.load(f) + +mc_version = config.get("minecraft_version", "1.21.10") +fabric_loader_version = config.get("fabric_loader_version", "0.17.3") +loader_meta_version = config.get("loader_meta_version", "1.1.0") +modrinth_mods = config.get("modrinth_mods", []) + +async def download_modrinth(mod_list: List[str], mc_ver: str): + downloaded_files = [] + failed_mods = [] + async with httpx.AsyncClient(timeout=60) as client: + for slug in mod_list: + try: + versions_url = f"https://api.modrinth.com/v2/project/{slug}/version" + resp = await client.get(versions_url) + if resp.status_code != 200: + print(f"[Modrinth] Could not get versions for {slug}") + failed_mods.append(slug) + continue + + versions = resp.json() + compatible_versions = [ + v for v in versions + if "fabric" in v.get("loaders", []) and mc_ver in v.get("game_versions", []) + ] + + if not compatible_versions: + print(f"[Modrinth] No Fabric versions found for {slug} on MC {mc_ver}") + failed_mods.append(slug) + continue + + latest = max(compatible_versions, key=lambda v: v["date_published"]) + file_info = latest.get("files", [])[0] + jar_url = file_info["url"] + filename = file_info["filename"] + + print(f"[Modrinth] Downloading {slug} -> {filename} ...") + jar_resp = await client.get(jar_url) + if jar_resp.status_code != 200: + print(f"[Modrinth] Couldn't download jar for {slug}") + failed_mods.append(slug) + continue + + path = os.path.join(mods_dir, filename) + with open(path, "wb") as f: + f.write(jar_resp.content) + + downloaded_files.append(path) + print(f"[Modrinth] Successfully downloaded: {filename}") + + except Exception as e: + print(f"[Modrinth] Error downloading {slug}: {e}") + failed_mods.append(slug) + + return downloaded_files, failed_mods + +async def download_fabric_server(mc_ver: str, loader_ver: str, meta_ver: str): + url = f"https://meta.fabricmc.net/v2/versions/loader/{mc_ver}/{loader_ver}/{meta_ver}/server/jar" + filename = "server.jar" + path = os.path.join(server_dir, filename) + + async with httpx.AsyncClient(timeout=60) as client: + try: + print(f"[Fabric] Downloading Fabric server for MC {mc_ver} ...") + resp = await client.get(url) + if resp.status_code != 200: + print(f"[Fabric] Failed to download Fabric server JAR: {resp.status_code}") + return None + + with open(path, "wb") as f: + f.write(resp.content) + + print(f"[Fabric] Successfully downloaded Fabric server: {filename}") + return path + except Exception as e: + print(f"[Fabric] Error downloading Fabric server: {e}") + return None + +async def run_fabric_server(): + jar_path = os.path.join(server_dir, "server.jar") + + if not os.path.exists(jar_path): + print("[Server] Server JAR not found, cannot start server.") + return + + eula_path = os.path.join(server_dir, "eula.txt") + if not os.path.exists(eula_path): + print("[Server] Accepting EULA automatically...") + with open(eula_path, "w") as f: + f.write("eula=true\n") + + print(f"[Server] Starting Fabric server from {jar_path} ...") + + process = subprocess.Popen( + ["java", "-jar", "server.jar", "nogui"], + cwd=server_dir + ) + process.wait() + print("[Server] Fabric server stopped.") + +def cleanup_server(delete_list: List[str]): + print("[Cleanup] Cleaning up server files...") + for item in delete_list: + path = os.path.join(server_dir, item) + if os.path.exists(path): + try: + if os.path.isdir(path): + shutil.rmtree(path) + print(f"[Cleanup] Removed folder: {item}") + else: + os.remove(path) + print(f"[Cleanup] Removed file: {item}") + except Exception as e: + print(f"[Cleanup] Failed to remove {item}: {e}") + else: + print(f"[Cleanup] {item} does not exist, skipping.") + print("[Cleanup] Cleanup complete.") + +async def main(): + print(f"Starting Benson Mod Updater for Fabric MC {mc_version}...\n") + files, failed_mods = await download_modrinth(modrinth_mods, mc_version) + server_file = await download_fabric_server(mc_version, fabric_loader_version, loader_meta_version) + + print("\nAll downloads complete!") + print(f"Downloaded {len(files)} mods:") + for f in files: + print(f" - {f}") + + if failed_mods: + print("\nThe following mods failed to download:") + for mod in failed_mods: + print(f" - {mod}") + else: + print("\nAll mods downloaded successfully!") + + if server_file: + await run_fabric_server() + cleanup_server(delete_list) + +if __name__ == "__main__": + asyncio.run(main()) -- cgit v1.3