Files
AstralRinth/apps/frontend/src/composables/pyroServers.ts
Evan Song d5f2ada8f7 Fix server suspension statuses (#3100)
* chore: correctly type suspension reason

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: faulty suspension condition allowing for fallthrough

Signed-off-by: Evan Song <theevansong@gmail.com>

* fix: here as well

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: add support suspension reason

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: handle support suspensions

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: patch pyroservers to handle 503

Signed-off-by: Evan Song <theevansong@gmail.com>

* feat: handle 503 in server root

Signed-off-by: Evan Song <theevansong@gmail.com>

* chore: dont make pyroservers errors scream at me anymore

Signed-off-by: Evan Song <theevansong@gmail.com>

---------

Signed-off-by: Evan Song <theevansong@gmail.com>
2025-01-06 22:06:34 -08:00

1453 lines
39 KiB
TypeScript

// usePyroServer is a composable that interfaces with the REDACTED API to get data and control the users server
import { $fetch, FetchError } from "ofetch";
interface PyroFetchOptions {
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
contentType?: string;
body?: Record<string, any>;
version?: number;
override?: {
url?: string;
token?: string;
};
retry?: boolean;
}
async function PyroFetch<T>(path: string, options: PyroFetchOptions = {}): Promise<T> {
const config = useRuntimeConfig();
const auth = await useAuth();
const authToken = auth.value?.token;
if (!authToken) {
throw new PyroFetchError("Cannot pyrofetch without auth", 10000);
}
const { method = "GET", contentType = "application/json", body, version = 0, override } = options;
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
/\/$/,
"",
);
if (!base) {
throw new PyroFetchError(
"Cannot pyrofetch without base url. Make sure to set a PYRO_BASE_URL in environment variables",
10001,
);
}
const fullUrl = override?.url
? `https://${override.url}/${path.replace(/^\//, "")}`
: `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`;
type HeadersRecord = Record<string, string>;
const headers: HeadersRecord = {
Authorization: `Bearer ${override?.token ?? authToken}`,
"Access-Control-Allow-Headers": "Authorization",
"User-Agent": "Pyro/1.0 (https://pyro.host)",
Vary: "Accept, Origin",
};
if (contentType !== "none") {
headers["Content-Type"] = contentType;
}
if (import.meta.client && typeof window !== "undefined") {
headers.Origin = window.location.origin;
}
try {
const response = await $fetch<T>(fullUrl, {
method,
headers,
body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined,
timeout: 10000,
retry: options.retry !== false ? (method === "GET" ? 3 : 0) : 0,
});
return response;
} catch (error) {
console.error("[PyroServers/PyroFetch]:", error);
if (error instanceof FetchError) {
const statusCode = error.response?.status;
const statusText = error.response?.statusText || "[no status text available]";
const errorMessages: { [key: number]: string } = {
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
429: "Too Many Requests",
500: "Internal Server Error",
502: "Bad Gateway",
503: "Service Unavailable",
};
const message =
statusCode && statusCode in errorMessages
? errorMessages[statusCode]
: `HTTP Error: ${statusCode || "[unhandled status code]"} ${statusText}`;
throw new PyroFetchError(`[PyroServers/PyroFetch] ${message}`, statusCode, error);
}
throw new PyroFetchError(
"[PyroServers/PyroFetch] An unexpected error occurred during the fetch operation.",
undefined,
error as Error,
);
}
}
const internalServerRefrence = ref<any>(null);
interface License {
id: string;
name: string;
url: string;
}
interface DonationUrl {
id: string;
platform: string;
url: string;
}
interface GalleryItem {
url: string;
featured: boolean;
title: string;
description: string;
created: string;
ordering: number;
}
export interface Project {
slug: string;
title: string;
description: string;
categories: string[];
client_side: "required" | "optional";
server_side: "required" | "optional";
body: string;
status: "approved" | "pending" | "rejected";
requested_status: "approved" | "pending" | "rejected";
additional_categories: string[];
issues_url: string;
source_url: string;
wiki_url: string;
discord_url: string;
donation_urls: DonationUrl[];
project_type: "mod" | "resourcepack" | "map" | "plugin";
downloads: number;
icon_url: string;
color: number;
thread_id: string;
monetization_status: "monetized" | "non-monetized";
id: string;
team: string;
body_url: string | null;
moderator_message: string | null;
published: string;
updated: string;
approved: string;
queued: string;
followers: number;
license: License;
versions: string[];
game_versions: string[];
loaders: string[];
gallery: GalleryItem[];
}
interface General {
server_id: string;
name: string;
net: {
ip: string;
port: number;
domain: string;
};
game: string;
backup_quota: number;
used_backup_quota: number;
status: string;
suspension_reason:
| "moderated"
| "paymentfailed"
| "cancelled"
| "other"
| "transferring"
| "upgrading"
| "support"
| (string & {});
loader: string;
loader_version: string;
mc_version: string;
upstream: {
kind: "modpack" | "mod" | "resourcepack";
version_id: string;
project_id: string;
} | null;
motd?: string;
image?: string;
project?: Project;
sftp_username: string;
sftp_password: string;
sftp_host: string;
datacenter?: string;
}
interface Allocation {
port: number;
name: string;
}
interface Startup {
invocation: string;
original_invocation: string;
jdk_version: "lts8" | "lts11" | "lts17" | "lts21";
jdk_build: "corretto" | "temurin" | "graal";
}
export interface Mod {
filename: string;
project_id: string | undefined;
version_id: string | undefined;
name: string | undefined;
version_number: string | undefined;
icon_url: string | undefined;
owner: string | undefined;
disabled: boolean;
installing: boolean;
}
interface Backup {
id: string;
name: string;
created_at: string;
ongoing: boolean;
locked: boolean;
}
interface AutoBackupSettings {
enabled: boolean;
interval: number;
}
interface JWTAuth {
url: string;
token: string;
}
export interface DirectoryItem {
name: string;
type: "directory" | "file";
count?: number;
modified: number;
created: number;
path: string;
}
export interface DirectoryResponse {
items: DirectoryItem[];
total: number;
current?: number;
}
type ContentType = "Mod" | "Plugin";
const 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;
};
const processImage = async (iconUrl: string | undefined) => {
const image = ref<string | null>(null);
const auth = await PyroFetch<JWTAuth>(`servers/${internalServerRefrence.value.serverId}/fs`);
try {
const fileData = await PyroFetch(`/download?path=/server-icon-original.png`, {
override: auth,
retry: false,
});
if (fileData instanceof Blob) {
if (import.meta.client) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.src = URL.createObjectURL(fileData);
await new Promise<void>((resolve) => {
img.onload = () => {
canvas.width = 512;
canvas.height = 512;
ctx?.drawImage(img, 0, 0, 512, 512);
const dataURL = canvas.toDataURL("image/png");
internalServerRefrence.value.general.image = dataURL;
image.value = dataURL;
resolve();
};
});
}
}
} catch (error) {
if (error instanceof PyroFetchError && error.statusCode === 404) {
console.log("[PYROSERVERS] No server icon found");
} else {
console.error(error);
}
}
if (image.value === null && iconUrl) {
console.log("iconUrl", iconUrl);
try {
const response = await fetch(iconUrl);
const file = await response.blob();
const originalfile = new File([file], "server-icon-original.png", {
type: "image/png",
});
if (import.meta.client) {
const scaledFile = await new Promise<File>((resolve, reject) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const img = new Image();
img.src = URL.createObjectURL(file);
img.onload = () => {
canvas.width = 64;
canvas.height = 64;
ctx?.drawImage(img, 0, 0, 64, 64);
canvas.toBlob((blob) => {
if (blob) {
const data = new File([blob], "server-icon.png", { type: "image/png" });
resolve(data);
} else {
reject(new Error("Canvas toBlob failed"));
}
}, "image/png");
};
img.onerror = reject;
});
if (scaledFile) {
await PyroFetch(`/create?path=/server-icon.png&type=file`, {
method: "POST",
contentType: "application/octet-stream",
body: scaledFile,
override: auth,
});
await PyroFetch(`/create?path=/server-icon-original.png&type=file`, {
method: "POST",
contentType: "application/octet-stream",
body: originalfile,
override: auth,
});
}
}
} catch (error) {
if (error instanceof PyroFetchError && error.statusCode === 404) {
console.log("[PYROSERVERS] No server icon found");
} else {
console.error(error);
}
}
}
return image.value;
};
// ------------------ GENERAL ------------------ //
const sendPowerAction = async (action: string) => {
try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/power`, {
method: "POST",
body: { action },
});
await new Promise((resolve) => setTimeout(resolve, 1000));
await internalServerRefrence.value.refresh();
} catch (error) {
console.error("Error changing power state:", error);
throw error;
}
};
const updateName = async (newName: string) => {
try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/name`, {
method: "POST",
body: { name: newName },
});
} catch (error) {
console.error("Error updating server name:", error);
throw error;
}
};
const reinstallServer = async (
serverId: string,
loader: boolean,
projectId: string,
versionId?: string,
loaderVersionId?: string,
hardReset: boolean = false,
) => {
try {
const hardResetParam = hardReset ? "true" : "false";
if (loader) {
if (projectId.toLowerCase() === "neoforge") {
projectId = "NeoForge";
}
await PyroFetch(`servers/${serverId}/reinstall?hard=${hardResetParam}`, {
method: "POST",
body: { loader: projectId, loader_version: loaderVersionId, game_version: versionId },
});
} else {
await PyroFetch(`servers/${serverId}/reinstall?hard=${hardResetParam}`, {
method: "POST",
body: { project_id: projectId, version_id: versionId },
});
}
} catch (error) {
console.error("Error reinstalling server:", error);
throw error;
}
};
const reinstallFromMrpack = async (mrpack: File, hardReset: boolean = false) => {
const hardResetParam = hardReset ? "true" : "false";
try {
const auth = await PyroFetch<JWTAuth>(
`servers/${internalServerRefrence.value.serverId}/reinstallFromMrpack`,
);
const formData = new FormData();
formData.append("file", mrpack);
const response = await fetch(
`https://${auth.url}/reinstallMrpackMultiparted?hard=${hardResetParam}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${auth.token}`,
},
body: formData,
signal: AbortSignal.timeout(30 * 60 * 1000),
},
);
if (!response.ok) {
throw new Error(`[pyroservers] native fetch err status: ${response.status}`);
}
} catch (error) {
console.error("Error reinstalling from mrpack:", error);
throw error;
}
};
const suspendServer = async (status: boolean) => {
try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/suspend`, {
method: "POST",
body: { suspended: status },
});
} catch (error) {
console.error("Error suspending server:", error);
throw error;
}
};
const fetchConfigFile = async (fileName: string) => {
try {
return await PyroFetch(`servers/${internalServerRefrence.value.serverId}/config/${fileName}`);
} catch (error) {
console.error("Error fetching config file:", error);
throw error;
}
};
const getMotd = async () => {
try {
const props = await downloadFile("/server.properties");
if (props) {
const lines = props.split("\n");
for (const line of lines) {
if (line.startsWith("motd=")) {
return line.slice(5);
}
}
}
} catch {
return null;
}
};
const setMotd = async (motd: string) => {
try {
const props = (await fetchConfigFile("ServerProperties")) as any;
if (props) {
props.motd = motd;
const newProps = constructServerProperties(props);
const octetStream = new Blob([newProps], { type: "application/octet-stream" });
const auth = await await PyroFetch<JWTAuth>(
`servers/${internalServerRefrence.value.serverId}/fs`,
);
return await PyroFetch(`/update?path=/server.properties`, {
method: "PUT",
contentType: "application/octet-stream",
body: octetStream,
override: auth,
});
}
} catch (error) {
console.error("Error setting motd:", error);
}
};
// ------------------ CONTENT ------------------ //
const installContent = async (contentType: ContentType, projectId: string, versionId: string) => {
try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/mods`, {
method: "POST",
body: {
install_as: contentType,
rinth_ids: { project_id: projectId, version_id: versionId },
},
});
} catch (error) {
console.error("Error installing mod:", error);
throw error;
}
};
const removeContent = async (contentType: ContentType, contentId: string) => {
try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/deleteMod`, {
method: "POST",
body: {
install_as: contentType,
path: contentId,
},
});
} catch (error) {
console.error("Error removing mod:", error);
throw error;
}
};
const reinstallContent = async (
contentType: ContentType,
contentId: string,
newContentId: string,
) => {
try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/mods/${contentId}`, {
method: "PUT",
body: { install_as: contentType, version_id: newContentId },
});
} catch (error) {
console.error("Error reinstalling mod:", error);
throw error;
}
};
// ------------------ BACKUPS ------------------ //
const createBackup = async (backupName: string) => {
try {
const response = (await PyroFetch(`servers/${internalServerRefrence.value.serverId}/backups`, {
method: "POST",
body: { name: backupName },
})) as { id: string };
return response.id;
} catch (error) {
console.error("Error creating backup:", error);
throw error;
}
};
const renameBackup = async (backupId: string, newName: string) => {
try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/backups/${backupId}/rename`, {
method: "POST",
body: { name: newName },
});
} catch (error) {
console.error("Error renaming backup:", error);
throw error;
}
};
const deleteBackup = async (backupId: string) => {
try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/backups/${backupId}`, {
method: "DELETE",
});
} catch (error) {
console.error("Error deleting backup:", error);
throw error;
}
};
const restoreBackup = async (backupId: string) => {
try {
await PyroFetch(
`servers/${internalServerRefrence.value.serverId}/backups/${backupId}/restore`,
{
method: "POST",
},
);
} catch (error) {
console.error("Error restoring backup:", error);
throw error;
}
};
const downloadBackup = async (backupId: string) => {
try {
return await PyroFetch(`servers/${internalServerRefrence.value.serverId}/backups/${backupId}`);
} catch (error) {
console.error("Error downloading backup:", error);
throw error;
}
};
const updateAutoBackup = async (autoBackup: "enable" | "disable", interval: number) => {
try {
return await PyroFetch(`servers/${internalServerRefrence.value.serverId}/autobackup`, {
method: "POST",
body: { set: autoBackup, interval },
});
} catch (error) {
console.error("Error updating auto backup:", error);
throw error;
}
};
const getAutoBackup = async () => {
try {
return await PyroFetch(`servers/${internalServerRefrence.value.serverId}/autobackup`);
} catch (error) {
console.error("Error getting auto backup settings:", error);
throw error;
}
};
const lockBackup = async (backupId: string) => {
try {
return await PyroFetch(
`servers/${internalServerRefrence.value.serverId}/backups/${backupId}/lock`,
{
method: "POST",
},
);
} catch (error) {
console.error("Error locking backup:", error);
throw error;
}
};
const unlockBackup = async (backupId: string) => {
try {
return await PyroFetch(
`servers/${internalServerRefrence.value.serverId}/backups/${backupId}/unlock`,
{
method: "POST",
},
);
} catch (error) {
console.error("Error locking backup:", error);
throw error;
}
};
// ------------------ NETWORK ------------------ //
const reserveAllocation = async (name: string): Promise<Allocation> => {
try {
return await PyroFetch<Allocation>(
`servers/${internalServerRefrence.value.serverId}/allocations?name=${name}`,
{
method: "POST",
},
);
} catch (error) {
console.error("Error reserving new allocation:", error);
throw error;
}
};
const updateAllocation = async (port: number, name: string) => {
try {
await PyroFetch(
`servers/${internalServerRefrence.value.serverId}/allocations/${port}?name=${name}`,
{
method: "PUT",
},
);
} catch (error) {
console.error("Error updating allocations:", error);
throw error;
}
};
const deleteAllocation = async (port: number) => {
try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/allocations/${port}`, {
method: "DELETE",
});
} catch (error) {
console.error("Error deleting allocation:", error);
throw error;
}
};
const checkSubdomainAvailability = async (subdomain: string): Promise<{ available: boolean }> => {
try {
return (await PyroFetch(`subdomains/${subdomain}/isavailable`)) as { available: boolean };
} catch (error) {
console.error("Error checking subdomain availability:", error);
throw error;
}
};
const changeSubdomain = async (subdomain: string) => {
try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/subdomain`, {
method: "POST",
body: { subdomain },
});
} catch (error) {
console.error("Error changing subdomain:", error);
throw error;
}
};
// ------------------ STARTUP ------------------ //
const updateStartupSettings = async (
invocation: string,
jdkVersion: "lts8" | "lts11" | "lts17" | "lts21",
jdkBuild: "corretto" | "temurin" | "graal",
) => {
try {
await PyroFetch(`servers/${internalServerRefrence.value.serverId}/startup`, {
method: "POST",
body: {
invocation: invocation || null,
jdk_version: jdkVersion || null,
jdk_build: jdkBuild || null,
},
});
} catch (error) {
console.error("Error updating startup settings:", error);
throw error;
}
};
// ------------------ FS ------------------ //
const retryWithAuth = async (requestFn: () => Promise<any>) => {
try {
return await requestFn();
} catch (error) {
if (error instanceof PyroFetchError && error.statusCode === 401) {
await internalServerRefrence.value.refresh(["fs"]);
return await requestFn();
}
throw error;
}
};
const listDirContents = (path: string, page: number, pageSize: number) => {
return retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path);
return await PyroFetch(`/list?path=${encodedPath}&page=${page}&page_size=${pageSize}`, {
override: internalServerRefrence.value.fs.auth,
retry: false,
});
});
};
const createFileOrFolder = (path: string, type: "file" | "directory") => {
return retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path);
return await PyroFetch(`/create?path=${encodedPath}&type=${type}`, {
method: "POST",
contentType: "application/octet-stream",
override: internalServerRefrence.value.fs.auth,
});
});
};
const uploadFile = (path: string, file: File) => {
// eslint-disable-next-line require-await
return retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path);
const progressSubject = new EventTarget();
const abortController = new AbortController();
const uploadPromise = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
const progress = (e.loaded / e.total) * 100;
progressSubject.dispatchEvent(
new CustomEvent("progress", {
detail: {
loaded: e.loaded,
total: e.total,
progress,
},
}),
);
}
});
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject(new Error(`Upload failed with status ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error("Upload failed"));
xhr.onabort = () => reject(new Error("Upload cancelled"));
xhr.open(
"POST",
`https://${internalServerRefrence.value.fs.auth.url}/create?path=${encodedPath}&type=file`,
);
xhr.setRequestHeader("Authorization", `Bearer ${internalServerRefrence.value.fs.auth.token}`);
xhr.setRequestHeader("Content-Type", "application/octet-stream");
xhr.send(file);
abortController.signal.addEventListener("abort", () => {
xhr.abort();
});
});
return {
promise: uploadPromise,
onProgress: (
callback: (progress: { loaded: number; total: number; progress: number }) => void,
) => {
progressSubject.addEventListener("progress", ((e: CustomEvent) => {
callback(e.detail);
}) as EventListener);
},
cancel: () => {
abortController.abort();
},
};
});
};
const renameFileOrFolder = (path: string, name: string) => {
const pathName = path.split("/").slice(0, -1).join("/") + "/" + name;
return retryWithAuth(async () => {
await PyroFetch(`/move`, {
method: "POST",
override: internalServerRefrence.value.fs.auth,
body: {
source: path,
destination: pathName,
},
});
return true;
});
};
const updateFile = (path: string, content: string) => {
const octetStream = new Blob([content], { type: "application/octet-stream" });
return retryWithAuth(async () => {
return await PyroFetch(`/update?path=${path}`, {
method: "PUT",
contentType: "application/octet-stream",
body: octetStream,
override: internalServerRefrence.value.fs.auth,
});
});
};
const createMissingFolders = async (path: string) => {
if (path.startsWith("/")) {
path = path.substring(1);
}
const folders = path.split("/");
console.log(folders);
let currentPath = "";
for (const folder of folders) {
currentPath += "/" + folder;
try {
await createFileOrFolder(currentPath, "directory");
} catch {}
}
};
const moveFileOrFolder = (path: string, newPath: string) => {
return retryWithAuth(async () => {
console.log(path);
console.log(newPath);
console.log(newPath.substring(0, newPath.lastIndexOf("/")));
await createMissingFolders(newPath.substring(0, newPath.lastIndexOf("/")));
return await PyroFetch(`/move`, {
method: "POST",
override: internalServerRefrence.value.fs.auth,
body: {
source: path,
destination: newPath,
},
});
});
};
const deleteFileOrFolder = (path: string, recursive: boolean) => {
const encodedPath = encodeURIComponent(path);
return retryWithAuth(async () => {
return await PyroFetch(`/delete?path=${encodedPath}&recursive=${recursive}`, {
method: "DELETE",
override: internalServerRefrence.value.fs.auth,
});
});
};
const downloadFile = (path: string, raw?: boolean) => {
return retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path);
const fileData = await PyroFetch(`/download?path=${encodedPath}`, {
override: internalServerRefrence.value.fs.auth,
});
if (fileData instanceof Blob) {
if (raw) {
return fileData;
} else {
return await fileData.text();
}
}
});
};
const modules: any = {
general: {
get: async (serverId: string) => {
try {
const data = await PyroFetch<General>(`servers/${serverId}`);
// TODO: temp hack to fix hydration error
if (data.upstream?.project_id) {
const res = await $fetch(
`https://api.modrinth.com/v2/project/${data.upstream.project_id}`,
);
data.project = res as Project;
}
if (import.meta.client) {
data.image = (await processImage(data.project?.icon_url)) ?? undefined;
}
const motd = await getMotd();
if (motd === "A Minecraft Server") {
await setMotd(
`§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`,
);
}
data.motd = motd;
return data;
} catch (error) {
internalServerRefrence.value.setError(error);
return undefined;
}
},
updateName,
power: sendPowerAction,
reinstall: reinstallServer,
reinstallFromMrpack,
suspend: suspendServer,
getMotd,
setMotd,
fetchConfigFile,
},
content: {
get: async (serverId: string) => {
try {
const mods = await PyroFetch<Mod[]>(`servers/${serverId}/mods`);
return {
data:
internalServerRefrence.value.error === undefined
? mods.sort((a, b) => (a?.name ?? "").localeCompare(b?.name ?? ""))
: [],
};
} catch (error) {
internalServerRefrence.value.setError(error);
return undefined;
}
},
install: installContent,
remove: removeContent,
reinstall: reinstallContent,
},
backups: {
get: async (serverId: string) => {
try {
return { data: await PyroFetch<Backup[]>(`servers/${serverId}/backups`) };
} catch (error) {
internalServerRefrence.value.setError(error);
return undefined;
}
},
create: createBackup,
rename: renameBackup,
delete: deleteBackup,
restore: restoreBackup,
download: downloadBackup,
updateAutoBackup,
getAutoBackup,
lock: lockBackup,
unlock: unlockBackup,
},
network: {
get: async (serverId: string) => {
try {
return { allocations: await PyroFetch<Allocation[]>(`servers/${serverId}/allocations`) };
} catch (error) {
internalServerRefrence.value.setError(error);
return undefined;
}
},
reserveAllocation,
updateAllocation,
deleteAllocation,
checkSubdomainAvailability,
changeSubdomain,
},
startup: {
get: async (serverId: string) => {
try {
return await PyroFetch<Startup>(`servers/${serverId}/startup`);
} catch (error) {
internalServerRefrence.value.setError(error);
return undefined;
}
},
update: updateStartupSettings,
},
ws: {
get: async (serverId: string) => {
try {
return await PyroFetch<JWTAuth>(`servers/${serverId}/ws`);
} catch (error) {
internalServerRefrence.value.setError(error);
return undefined;
}
},
},
fs: {
get: async (serverId: string) => {
try {
return { auth: await PyroFetch<JWTAuth>(`servers/${serverId}/fs`) };
} catch (error) {
internalServerRefrence.value.setError(error);
return undefined;
}
},
listDirContents,
createFileOrFolder,
uploadFile,
renameFileOrFolder,
updateFile,
moveFileOrFolder,
deleteFileOrFolder,
downloadFile,
},
};
type GeneralFunctions = {
/**
* INTERNAL: Gets the general settings of a server.
* @param serverId - The ID of the server.
*/
get: (serverId: string) => Promise<General>;
/**
* Updates the name of the server.
* @param newName - The new name for the server.
*/
updateName: (newName: string) => Promise<void>;
/**
* Sends a power action to the server.
* @param action - The power action to send (e.g., "start", "stop", "restart").
*/
power: (action: string) => Promise<void>;
/**
* Reinstalls the server with the specified project and version.
* @param loader - Whether to use a loader.
* @param projectId - The ID of the project.
* @param versionId - Optional version ID.
* @param loaderVersionId - Optional loader version ID.
* @param hardReset - Whether to perform a hard reset.
*/
reinstall: (
serverId: string,
loader: boolean,
projectId: string,
versionId?: string,
loaderVersionId?: string,
hardReset?: boolean,
) => Promise<void>;
/**
* Reinstalls the server from a mrpack.
* @param mrpack - The mrpack file.
* @param hardReset - Whether to perform a hard reset.
*/
reinstallFromMrpack: (mrpack: File, hardReset?: boolean) => Promise<void>;
/**
* Suspends or resumes the server.
* @param status - True to suspend the server, false to resume.
*/
suspend: (status: boolean) => Promise<void>;
/**
* INTERNAL: Gets the general settings of a server.
*/
getMotd: () => Promise<string>;
/**
* INTERNAL: Updates the general settings of a server.
* @param motd - The new motd.
*/
setMotd: (motd: string) => Promise<void>;
/**
* INTERNAL: Gets the config file of a server.
* @param fileName - The name of the file.
*/
fetchConfigFile: (fileName: string) => Promise<any>;
};
type ContentFunctions = {
/**
* INTERNAL: Gets the list content of a server.
* @param serverId - The ID of the server.
* @returns
*/
get: (serverId: string) => Promise<Mod[]>;
/**
* Installs a mod to a server.
* @param contentType - The type of content to install.
* @param projectId - The ID of the project.
* @param versionId - The ID of the version.
*/
install: (contentType: ContentType, projectId: string, versionId: string) => Promise<void>;
/**
* Removes a mod from a server.
* @param contentType - The type of content to remove.
* @param contentId - The ID of the content.
*/
remove: (contentType: ContentType, contentId: string) => Promise<void>;
/**
* Reinstalls a mod to a server.
* @param contentType - The type of content to reinstall.
* @param contentId - The ID of the content.
* @param newContentId - The ID of the new version.
*/
reinstall: (contentType: ContentType, contentId: string, newContentId: string) => Promise<void>;
};
type BackupFunctions = {
/**
* INTERNAL: Gets the backups of a server.
* @param serverId - The ID of the server.
* @returns
*/
get: (serverId: string) => Promise<Backup[]>;
/**
* Creates a new backup for the server.
* @param backupName - The name of the backup.
* @returns The ID of the backup.
*/
create: (backupName: string) => Promise<void>;
/**
* Renames a backup for the server.
* @param backupId - The ID of the backup.
* @param newName - The new name for the backup.
*/
rename: (backupId: string, newName: string) => Promise<void>;
/**
* Deletes a backup for the server.
* @param backupId - The ID of the backup.
*/
delete: (backupId: string) => Promise<void>;
/**
* Restores a backup for the server.
* @param serverId - The ID of the server.
* @param backupId - The ID of the backup.
*/
restore: (backupId: string) => Promise<void>;
/**
* Downloads a backup for the server.
* @param backupId - The ID of the backup.
*/
download: (backupId: string) => Promise<void>;
/**
* Updates the auto backup settings of the server.
* @param autoBackup - Whether to enable auto backup.
* @param interval - The interval to backup at (in Hours).
*/
updateAutoBackup: (autoBackup: "enable" | "disable", interval: number) => Promise<void>;
/**
* Gets the auto backup settings of the server.
*/
getAutoBackup: () => Promise<AutoBackupSettings>;
/**
* Locks a backup for the server.
* @param backupId - The ID of the backup.
*/
lock: (backupId: string) => Promise<void>;
/**
* Unlocks a backup for the server.
* @param backupId - The ID of the backup.
*/
unlock: (backupId: string) => Promise<void>;
};
type NetworkFunctions = {
/**
* INTERNAL: Gets the network settings of a server.
* @param serverId - The ID of the server.
* @returns
*/
get: (serverId: string) => Promise<Allocation[]>;
/**
* Reserves a new allocation for the server.
* @param name - The name of the allocation.
* @returns The allocated network port details.
*/
reserveAllocation: (name: string) => Promise<Allocation>;
/**
* Updates the allocation for the server.
* @param port - The port to update.
* @param name - The new name for the allocation.
*/
updateAllocation: (port: number, name: string) => Promise<void>;
/**
* Deletes an allocation for the server.
* @param port - The port to delete.
*/
deleteAllocation: (port: number) => Promise<void>;
/**
* Checks if a subdomain is available.
* @param subdomain - The subdomain to check.
* @returns True if the subdomain is available, otherwise false.
*/
checkSubdomainAvailability: (subdomain: string) => Promise<boolean>;
/**
* Changes the subdomain of the server.
* @param subdomain - The new subdomain.
*/
changeSubdomain: (subdomain: string) => Promise<void>;
};
type StartupFunctions = {
/**
* INTERNAL: Gets the startup settings of a server.
* @param serverId - The ID of the server.
* @returns
*/
get: (serverId: string) => Promise<Startup>;
/**
* Updates the startup settings of a server.
* @param invocation - The invocation of the server.
* @param jdkVersion - The version of the JDK.
* @param jdkBuild - The build of the JDK.
*/
update: (
invocation: string,
jdkVersion: "lts8" | "lts11" | "lts17" | "lts21",
jdkBuild: "corretto" | "temurin" | "graal",
) => Promise<void>;
};
type FSFunctions = {
/**
* INTERNAL: Gets the file system settings of a server.
* @param serverId
* @returns
*/
get: (serverId: string) => Promise<JWTAuth>;
/**
* @param path - The path to list the contents of.
* @param page - The page to list.
* @param pageSize - The page size to list.
* @returns
*/
listDirContents: (path: string, page: number, pageSize: number) => Promise<DirectoryResponse>;
/**
* @param path - The path to create the file or folder at.
* @param type - The type of file or folder to create.
* @returns
*/
createFileOrFolder: (path: string, type: "file" | "directory") => Promise<any>;
/**
* @param path - The path to upload the file to.
* @param file - The file to upload.
* @returns
*/
uploadFile: (path: string, file: File) => Promise<any>;
/**
* @param path - The path to rename the file or folder at.
* @param name - The new name for the file or folder.
* @returns
*/
renameFileOrFolder: (path: string, name: string) => Promise<any>;
/**
* @param path - The path to update the file at.
* @param content - The new content for the file.
* @returns
*/
updateFile: (path: string, content: string) => Promise<any>;
/**
* @param path - The path to move the file or folder at.
* @param newPath - The new path for the file or folder.
* @returns
*/
moveFileOrFolder: (path: string, newPath: string) => Promise<any>;
/**
* @param path - The path to delete the file or folder at.
* @param recursive - Whether to delete the file or folder recursively.
* @returns
*/
deleteFileOrFolder: (path: string, recursive: boolean) => Promise<any>;
/**
* @param serverId - The ID of the server.
* @param path - The path to download the file from.
* @param raw - Whether to return the raw blob.
* @returns
*/
downloadFile: (path: string, raw?: boolean) => Promise<any>;
};
type GeneralModule = General & GeneralFunctions;
type ContentModule = { data: Mod[] } & ContentFunctions;
type BackupsModule = { data: Backup[] } & BackupFunctions;
type NetworkModule = { allocations: Allocation[] } & NetworkFunctions;
type StartupModule = Startup & StartupFunctions;
export type FSModule = { auth: JWTAuth } & FSFunctions;
type ModulesMap = {
general: GeneralModule;
content: ContentModule;
backups: BackupsModule;
network: NetworkModule;
startup: StartupModule;
ws: JWTAuth;
fs: FSModule;
};
type avaliableModules = ("general" | "content" | "backups" | "network" | "startup" | "ws" | "fs")[];
export type Server<T extends avaliableModules> = {
[K in T[number]]?: ModulesMap[K];
} & {
/**
* Refreshes the included modules of the server
* @param refreshModules - The modules to refresh.
*/
refresh: (refreshModules?: avaliableModules) => Promise<void>;
setError: (error: Error) => void;
error?: Error;
serverId: string;
};
export const usePyroServer = async (serverId: string, includedModules: avaliableModules) => {
const server: Server<typeof includedModules> = reactive({
refresh: async (refreshModules?: avaliableModules) => {
const promises: Promise<void>[] = [];
if (refreshModules) {
for (const module of refreshModules) {
promises.push(
(async () => {
const mods = modules[module];
if (mods.get) {
const data = await mods.get(serverId);
server[module] = { ...server[module], ...data };
}
})(),
);
}
} else {
for (const module of includedModules) {
promises.push(
(async () => {
const mods = modules[module];
if (mods.get) {
const data = await mods.get(serverId);
server[module] = { ...server[module], ...data };
}
})(),
);
}
}
await Promise.all(promises);
},
setError: (error: Error) => {
server.error = error;
},
serverId,
});
for (const module of includedModules) {
const mods = modules[module];
server[module] = mods;
}
internalServerRefrence.value = server;
await server.refresh();
return server as Server<typeof includedModules>;
};