import { ModrinthServerError } from "@modrinth/utils"; import type { JWTAuth, ModuleError, ModuleName } from "@modrinth/utils"; import { useServersFetch } from "./servers-fetch.ts"; import { GeneralModule, ContentModule, BackupsModule, NetworkModule, StartupModule, WSModule, FSModule, } from "./modules/index.ts"; export function handleError(err: any) { if (err instanceof ModrinthServerError && err.v1Error) { addNotification({ title: err.v1Error?.context ?? `An error occurred`, type: "error", text: err.v1Error.description, errorCode: err.v1Error.error, }); } else { addNotification({ title: "An error occurred", type: "error", text: err.message ?? (err.data ? err.data.description : err), }); } } export class ModrinthServer { readonly serverId: string; private errors: Partial> = {}; readonly general: GeneralModule; readonly content: ContentModule; readonly backups: BackupsModule; readonly network: NetworkModule; readonly startup: StartupModule; readonly ws: WSModule; readonly fs: FSModule; constructor(serverId: string) { this.serverId = serverId; this.general = new GeneralModule(this); this.content = new ContentModule(this); this.backups = new BackupsModule(this); this.network = new NetworkModule(this); this.startup = new StartupModule(this); this.ws = new WSModule(this); this.fs = new FSModule(this); } async createMissingFolders(path: string): Promise { if (path.startsWith("/")) { path = path.substring(1); } const folders = path.split("/"); let currentPath = ""; for (const folder of folders) { currentPath += "/" + folder; try { await this.fs.createFileOrFolder(currentPath, "directory"); } catch { // Folder might already exist, ignore error } } } async fetchConfigFile(fileName: string): Promise { return await useServersFetch(`servers/${this.serverId}/config/${fileName}`); } constructServerProperties(properties: any): string { let fileContent = `#Minecraft server properties\n#${new Date().toUTCString()}\n`; for (const [key, value] of Object.entries(properties)) { if (typeof value === "object") { fileContent += `${key}=${JSON.stringify(value)}\n`; } else if (typeof value === "boolean") { fileContent += `${key}=${value ? "true" : "false"}\n`; } else { fileContent += `${key}=${value}\n`; } } return fileContent; } async processImage(iconUrl: string | undefined): Promise { const sharedImage = useState(`server-icon-${this.serverId}`); if (sharedImage.value) { return sharedImage.value; } try { const auth = await useServersFetch(`servers/${this.serverId}/fs`); try { const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, { override: auth, retry: false, }); if (fileData instanceof Blob && import.meta.client) { const dataURL = await new Promise((resolve) => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const img = new Image(); img.onload = () => { canvas.width = 512; canvas.height = 512; ctx?.drawImage(img, 0, 0, 512, 512); const dataURL = canvas.toDataURL("image/png"); sharedImage.value = dataURL; resolve(dataURL); URL.revokeObjectURL(img.src); }; img.src = URL.createObjectURL(fileData); }); return dataURL; } } catch (error) { if (error instanceof ModrinthServerError && error.statusCode === 404 && iconUrl) { // Handle external icon processing try { const response = await fetch(iconUrl); if (!response.ok) throw new Error("Failed to fetch icon"); const file = await response.blob(); const originalFile = new File([file], "server-icon-original.png", { type: "image/png", }); if (import.meta.client) { const dataURL = await new Promise((resolve) => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const img = new Image(); img.onload = () => { canvas.width = 64; canvas.height = 64; ctx?.drawImage(img, 0, 0, 64, 64); canvas.toBlob(async (blob) => { if (blob) { const scaledFile = new File([blob], "server-icon.png", { type: "image/png" }); await useServersFetch(`/create?path=/server-icon.png&type=file`, { method: "POST", contentType: "application/octet-stream", body: scaledFile, override: auth, }); await useServersFetch(`/create?path=/server-icon-original.png&type=file`, { method: "POST", contentType: "application/octet-stream", body: originalFile, override: auth, }); } }, "image/png"); const dataURL = canvas.toDataURL("image/png"); sharedImage.value = dataURL; resolve(dataURL); URL.revokeObjectURL(img.src); }; img.src = URL.createObjectURL(file); }); return dataURL; } } catch (error) { console.error("Failed to process external icon:", error); } } } } catch (error) { console.error("Failed to process server icon:", error); } sharedImage.value = undefined; return undefined; } async refresh( modules: ModuleName[] = [], options?: { preserveConnection?: boolean; preserveInstallState?: boolean; }, ): Promise { const modulesToRefresh = modules.length > 0 ? modules : (["general", "content", "backups", "network", "startup", "ws", "fs"] as ModuleName[]); for (const module of modulesToRefresh) { try { switch (module) { case "general": { if (options?.preserveConnection) { const currentImage = this.general.image; const currentMotd = this.general.motd; const currentStatus = this.general.status; await this.general.fetch(); if (currentImage) { this.general.image = currentImage; } if (currentMotd) { this.general.motd = currentMotd; } if (options.preserveInstallState && currentStatus === "installing") { this.general.status = "installing"; } } else { await this.general.fetch(); } break; } case "content": await this.content.fetch(); break; case "backups": await this.backups.fetch(); break; case "network": await this.network.fetch(); break; case "startup": await this.startup.fetch(); break; case "ws": await this.ws.fetch(); break; case "fs": await this.fs.fetch(); break; } } catch (error) { this.errors[module] = { error: error instanceof ModrinthServerError ? error : new ModrinthServerError("Unknown error", undefined, error as Error), timestamp: Date.now(), }; } } } get moduleErrors() { return this.errors; } } export const useModrinthServers = async ( serverId: string, includedModules: ModuleName[] = ["general"], ) => { const server = new ModrinthServer(serverId); await server.refresh(includedModules); return reactive(server); };