Merge commit '74cf3f076eff43755bb4bef62f1c1bb3fc0e6c2a' into feature-clean

This commit is contained in:
2025-05-26 17:59:09 +03:00
497 changed files with 15033 additions and 9421 deletions

View File

@@ -1,6 +0,0 @@
export const useUserCountry = () =>
useState("userCountry", () => {
const headers = useRequestHeaders(["cf-ipcountry"]);
return headers["cf-ipcountry"] ?? "US";
});

View File

@@ -0,0 +1,36 @@
import { useState, useRequestHeaders } from "#imports";
export const useUserCountry = () => {
const country = useState<string>("userCountry", () => "US");
const fromServer = useState<boolean>("userCountryFromServer", () => false);
if (import.meta.server) {
const headers = useRequestHeaders(["cf-ipcountry", "accept-language"]);
const cf = headers["cf-ipcountry"];
if (cf) {
country.value = cf.toUpperCase();
fromServer.value = true;
} else {
const al = headers["accept-language"] || "";
const tag = al.split(",")[0];
const val = tag.split("-")[1]?.toLowerCase();
if (val) {
country.value = val;
fromServer.value = true;
}
}
}
if (import.meta.client) {
onMounted(() => {
if (fromServer.value) return;
const lang = navigator.language || navigator.userLanguage || "";
const region = lang.split("-")[1];
if (region) {
country.value = region.toUpperCase();
}
});
}
return country;
};

View File

@@ -1,17 +0,0 @@
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
dayjs.extend(relativeTime); // eslint-disable-line import/no-named-as-default-member
export const useCurrentDate = () => useState("currentDate", () => Date.now());
export const updateCurrentDate = () => {
const currentDate = useCurrentDate();
currentDate.value = Date.now();
};
export const fromNow = (date) => {
const currentDate = useCurrentDate();
return dayjs(date).from(currentDate.value);
};

View File

@@ -1,18 +0,0 @@
import { createFormatter, type Formatter } from "@vintl/how-ago";
import type { IntlController } from "@vintl/vintl/controller";
const formatters = new WeakMap<IntlController<any>, Formatter>();
export function useRelativeTime(): Formatter {
const vintl = useVIntl();
let formatter = formatters.get(vintl);
if (formatter == null) {
const formatterRef = computed(() => createFormatter(vintl.intl));
formatter = (value, options) => formatterRef.value(value, options);
formatters.set(vintl, formatter);
}
return formatter;
}

View File

@@ -11,11 +11,13 @@ export const addNotification = (notification) => {
);
if (existingNotif) {
setNotificationTimer(existingNotif);
existingNotif.count++;
return;
}
notification.id = new Date();
notification.count = 1;
setNotificationTimer(notification);
notifications.value.push(notification);

View File

@@ -1,7 +1,7 @@
// usePyroServer is a composable that interfaces with the REDACTED API to get data and control the users server
import { $fetch, FetchError } from "ofetch";
import type { ServerNotice } from "@modrinth/utils";
import type { WSBackupState, WSBackupTask } from "~/types/servers.ts";
import type { FilesystemOp, FSQueuedOp, WSBackupState, WSBackupTask } from "~/types/servers.ts";
interface PyroFetchOptions {
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -40,12 +40,19 @@ class PyroServerError extends Error {
}
}
export class PyroServersFetchError extends Error {
type V1ErrorInfo = {
context?: string;
error: string;
description: string;
};
export class ServersError extends Error {
constructor(
message: string,
public readonly statusCode?: number,
public readonly originalError?: Error,
public readonly module?: string,
public readonly v1Error?: V1ErrorInfo,
) {
let errorMessage = message;
let method = "GET";
@@ -96,17 +103,35 @@ export class PyroServersFetchError extends Error {
}
}
export const handleError = (err: any) => {
if (err instanceof ServersError && 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),
});
}
};
async function PyroFetch<T>(
path: string,
options: PyroFetchOptions = {},
module?: string,
errorContext?: string,
): Promise<T> {
const config = useRuntimeConfig();
const auth = await useAuth();
const authToken = auth.value?.token;
if (!authToken) {
throw new PyroServersFetchError("Missing auth token", 401, undefined, module);
throw new ServersError("Missing auth token", 401, undefined, module);
}
const {
@@ -124,16 +149,18 @@ async function PyroFetch<T>(
);
if (!base) {
throw new PyroServersFetchError(
"Configuration error: Missing PYRO_BASE_URL",
500,
undefined,
module,
);
throw new ServersError("Configuration error: Missing PYRO_BASE_URL", 500, undefined, module);
}
const fullUrl = override?.url
? `https://${override.url}/${path.replace(/^\//, "")}`
const versionString = `v${version}`;
let newOverrideUrl = override?.url;
if (newOverrideUrl && newOverrideUrl.includes("v0") && version !== 0) {
newOverrideUrl = newOverrideUrl.replace("v0", versionString);
}
const fullUrl = newOverrideUrl
? `https://${newOverrideUrl}/${path.replace(/^\//, "")}`
: `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`;
const headers: Record<string, string> = {
@@ -170,11 +197,20 @@ async function PyroFetch<T>(
attempts++;
if (error instanceof FetchError) {
let v1Error: V1ErrorInfo | undefined;
if (error.data.error && error.data.description) {
v1Error = {
context: errorContext,
...error.data,
};
}
const statusCode = error.response?.status;
const isRetryable = statusCode ? [408, 429, 500, 502, 503, 504].includes(statusCode) : true;
if (!isRetryable || attempts >= maxAttempts) {
throw new PyroServersFetchError(error.message, statusCode, error, module);
throw new ServersError(error.message, statusCode, error, module, v1Error);
}
const delay = Math.min(1000 * Math.pow(2, attempts - 1) + Math.random() * 1000, 10000);
@@ -182,7 +218,7 @@ async function PyroFetch<T>(
continue;
}
throw new PyroServersFetchError(
throw new ServersError(
"Unexpected error during fetch operation",
undefined,
error as Error,
@@ -271,10 +307,8 @@ interface General {
| "moderated"
| "paymentfailed"
| "cancelled"
| "other"
| "transferring"
| "upgrading"
| "support"
| "other"
| (string & {});
loader: string;
loader_version: string;
@@ -419,7 +453,7 @@ const processImage = async (iconUrl: string | undefined) => {
}
}
} catch (error) {
if (error instanceof PyroServersFetchError && error.statusCode === 404 && iconUrl) {
if (error instanceof ServersError && error.statusCode === 404 && iconUrl) {
try {
const response = await fetch(iconUrl);
if (!response.ok) throw new Error("Failed to fetch icon");
@@ -892,7 +926,7 @@ const retryWithAuth = async (requestFn: () => Promise<any>) => {
try {
return await requestFn();
} catch (error) {
if (error instanceof PyroServersFetchError && error.statusCode === 401) {
if (error instanceof ServersError && error.statusCode === 401) {
await internalServerReference.value.refresh(["fs"]);
return await requestFn();
}
@@ -1051,6 +1085,68 @@ const moveFileOrFolder = (path: string, newPath: string) => {
});
};
const clearQueuedOps = () => {
internalServerReference.value.fs.queuedOps = [];
};
const removeQueuedOp = (op: FSQueuedOp["op"], src: string) => {
internalServerReference.value.fs.queuedOps = internalServerReference.value.fs.queuedOps.filter(
(x: FSQueuedOp) => x.op !== op || x.src !== src,
);
};
const extractFile = (path: string, override = true, dry = false, silentQueue = false) =>
retryWithAuth(async () => {
console.log(
`Extracting: ${path}` + (dry ? " (dry run)" : "") + (silentQueue ? " (silent)" : ""),
);
const encodedPath = encodeURIComponent(path);
if (!silentQueue) {
internalServerReference.value.fs.queuedOps.push({
op: "unarchive",
src: path,
});
setTimeout(() => internalServerReference.value.fs.removeQueuedOp("unarchive", path), 4000);
}
return (await PyroFetch(
`/unarchive?src=${encodedPath}&trg=/&override=${override}&dry=${dry}`,
{
method: "POST",
override: internalServerReference.value.fs.auth,
version: 1,
},
undefined,
"Error extracting file",
).catch((err) => {
removeQueuedOp("unarchive", path);
throw err;
})) as { modpack_name: string | null };
});
const modifyOp = (id: string, action: "dismiss" | "cancel") =>
retryWithAuth(async () => {
return await PyroFetch(
`/ops/${action}?id=${id}`,
{
method: "POST",
override: internalServerReference.value.fs.auth,
version: 1,
},
undefined,
`Error ${action === "dismiss" ? "dismissing" : "cancelling"} filesystem operation`,
).then(() => {
internalServerReference.value.fs.opsQueuedForModification =
internalServerReference.value.fs.opsQueuedForModification.filter((x: string) => x !== id);
internalServerReference.value.fs.ops = internalServerReference.value.fs.ops.filter(
(x: FilesystemOp) => x.id !== id,
);
});
});
const deleteFileOrFolder = (path: string, recursive: boolean) => {
const encodedPath = encodeURIComponent(path);
return retryWithAuth(async () => {
@@ -1104,9 +1200,9 @@ const modules: any = {
return data;
} catch (error) {
const fetchError =
error instanceof PyroServersFetchError
error instanceof ServersError
? error
: new PyroServersFetchError("Unknown error occurred", undefined, error as Error);
: new ServersError("Unknown error occurred", undefined, error as Error);
return {
status: "error",
@@ -1135,9 +1231,9 @@ const modules: any = {
};
} catch (error) {
const fetchError =
error instanceof PyroServersFetchError
error instanceof ServersError
? error
: new PyroServersFetchError("Unknown error occurred", undefined, error as Error);
: new ServersError("Unknown error occurred", undefined, error as Error);
return {
data: [],
@@ -1160,9 +1256,9 @@ const modules: any = {
};
} catch (error) {
const fetchError =
error instanceof PyroServersFetchError
error instanceof ServersError
? error
: new PyroServersFetchError("Unknown error occurred", undefined, error as Error);
: new ServersError("Unknown error occurred", undefined, error as Error);
return {
data: [],
@@ -1196,9 +1292,9 @@ const modules: any = {
};
} catch (error) {
const fetchError =
error instanceof PyroServersFetchError
error instanceof ServersError
? error
: new PyroServersFetchError("Unknown error occurred", undefined, error as Error);
: new ServersError("Unknown error occurred", undefined, error as Error);
return {
allocations: [],
@@ -1221,9 +1317,9 @@ const modules: any = {
return await PyroFetch<Startup>(`servers/${serverId}/startup`, {}, "startup");
} catch (error) {
const fetchError =
error instanceof PyroServersFetchError
error instanceof ServersError
? error
: new PyroServersFetchError("Unknown error occurred", undefined, error as Error);
: new ServersError("Unknown error occurred", undefined, error as Error);
return {
error: {
@@ -1241,9 +1337,9 @@ const modules: any = {
return await PyroFetch<JWTAuth>(`servers/${serverId}/ws`, {}, "ws");
} catch (error) {
const fetchError =
error instanceof PyroServersFetchError
error instanceof ServersError
? error
: new PyroServersFetchError("Unknown error occurred", undefined, error as Error);
: new ServersError("Unknown error occurred", undefined, error as Error);
return {
error: {
@@ -1255,14 +1351,16 @@ const modules: any = {
},
},
fs: {
queuedOps: [],
opsQueuedForModification: [],
get: async (serverId: string) => {
try {
return { auth: await PyroFetch<JWTAuth>(`servers/${serverId}/fs`, {}, "fs") };
} catch (error) {
const fetchError =
error instanceof PyroServersFetchError
error instanceof ServersError
? error
: new PyroServersFetchError("Unknown error occurred", undefined, error as Error);
: new ServersError("Unknown error occurred", undefined, error as Error);
return {
auth: undefined,
@@ -1281,6 +1379,10 @@ const modules: any = {
moveFileOrFolder,
deleteFileOrFolder,
downloadFile,
extractFile,
removeQueuedOp,
clearQueuedOps,
modifyOp,
},
};
@@ -1588,10 +1690,29 @@ type FSFunctions = {
* @returns
*/
downloadFile: (path: string, raw?: boolean) => Promise<any>;
/**
* @param path - The path of the file to extract
* @returns
*/
extractFile: (
path: string,
override?: boolean,
dry?: boolean,
silentQueue?: boolean,
) => Promise<{
modpack_name: string | null;
conflicting_files: string[];
}>;
removeQueuedOp: (op: FSQueuedOp["op"], src: string) => void;
clearQueuedOps: () => void;
modifyOp: (id: string, action: "dismiss" | "cancel") => Promise<any>;
};
type ModuleError = {
error: PyroServersFetchError;
error: ServersError;
timestamp: number;
};
@@ -1624,8 +1745,11 @@ type WSModule = JWTAuth & {
error?: ModuleError;
};
type FSModule = {
export type FSModule = {
auth: JWTAuth;
ops: FilesystemOp[];
queuedOps: FSQueuedOp[];
opsQueuedForModification: string[];
error?: ModuleError;
} & FSFunctions;