You've already forked AstralRinth
forked from didirus/AstralRinth
Merge commit '74cf3f076eff43755bb4bef62f1c1bb3fc0e6c2a' into feature-clean
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
export const useUserCountry = () =>
|
||||
useState("userCountry", () => {
|
||||
const headers = useRequestHeaders(["cf-ipcountry"]);
|
||||
|
||||
return headers["cf-ipcountry"] ?? "US";
|
||||
});
|
||||
36
apps/frontend/src/composables/country.ts
Normal file
36
apps/frontend/src/composables/country.ts
Normal 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;
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user