refactor: migrate to common eslint+prettier configs (#4168)

* refactor: migrate to common eslint+prettier configs

* fix: prettier frontend

* feat: config changes

* fix: lint issues

* fix: lint

* fix: type imports

* fix: cyclical import issue

* fix: lockfile

* fix: missing dep

* fix: switch to tabs

* fix: continue switch to tabs

* fix: rustfmt parity

* fix: moderation lint issue

* fix: lint issues

* fix: ui intl

* fix: lint issues

* Revert "fix: rustfmt parity"

This reverts commit cb99d2376c321d813d4b7fc7e2a213bb30a54711.

* feat: revert last rs
This commit is contained in:
Cal H.
2025-08-14 21:48:38 +01:00
committed by GitHub
parent 82697278dc
commit 2aabcf36ee
702 changed files with 101360 additions and 102020 deletions

View File

@@ -1,141 +1,141 @@
import { injectNotificationManager } from "@modrinth/ui";
import { injectNotificationManager } from '@modrinth/ui'
export const useAuth = async (oldToken = null) => {
const auth = useState("auth", () => ({
user: null,
token: "",
headers: {},
}));
const auth = useState('auth', () => ({
user: null,
token: '',
headers: {},
}))
if (!auth.value.user || oldToken) {
auth.value = await initAuth(oldToken);
}
if (!auth.value.user || oldToken) {
auth.value = await initAuth(oldToken)
}
return auth;
};
return auth
}
export const initAuth = async (oldToken = null) => {
const auth = {
user: null,
token: "",
};
const auth = {
user: null,
token: '',
}
if (oldToken === "none") {
return auth;
}
if (oldToken === 'none') {
return auth
}
const route = useRoute();
const authCookie = useCookie("auth-token", {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: "lax",
secure: true,
httpOnly: false,
path: "/",
});
const route = useRoute()
const authCookie = useCookie('auth-token', {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
secure: true,
httpOnly: false,
path: '/',
})
if (oldToken) {
authCookie.value = oldToken;
}
if (oldToken) {
authCookie.value = oldToken
}
if (route.query.code && !route.fullPath.includes("new_account=true")) {
authCookie.value = route.query.code;
}
if (route.query.code && !route.fullPath.includes('new_account=true')) {
authCookie.value = route.query.code
}
if (route.fullPath.includes("new_account=true") && route.path !== "/auth/welcome") {
const redirect = route.path.startsWith("/auth/") ? null : route.fullPath;
if (route.fullPath.includes('new_account=true') && route.path !== '/auth/welcome') {
const redirect = route.path.startsWith('/auth/') ? null : route.fullPath
await navigateTo(
`/auth/welcome?authToken=${route.query.code}${
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ""
}`,
);
}
await navigateTo(
`/auth/welcome?authToken=${route.query.code}${
redirect ? `&redirect=${encodeURIComponent(redirect)}` : ''
}`,
)
}
if (authCookie.value) {
auth.token = authCookie.value;
if (authCookie.value) {
auth.token = authCookie.value
if (!auth.token || !auth.token.startsWith("mra_")) {
return auth;
}
if (!auth.token || !auth.token.startsWith('mra_')) {
return auth
}
try {
auth.user = await useBaseFetch(
"user",
{
headers: {
Authorization: auth.token,
},
},
true,
);
} catch {
/* empty */
}
}
try {
auth.user = await useBaseFetch(
'user',
{
headers: {
Authorization: auth.token,
},
},
true,
)
} catch {
/* empty */
}
}
if (!auth.user && auth.token) {
try {
const session = await useBaseFetch(
"session/refresh",
{
method: "POST",
headers: {
Authorization: auth.token,
},
},
true,
);
if (!auth.user && auth.token) {
try {
const session = await useBaseFetch(
'session/refresh',
{
method: 'POST',
headers: {
Authorization: auth.token,
},
},
true,
)
auth.token = session.session;
authCookie.value = auth.token;
auth.token = session.session
authCookie.value = auth.token
auth.user = await useBaseFetch(
"user",
{
headers: {
Authorization: auth.token,
},
},
true,
);
} catch {
authCookie.value = null;
}
}
auth.user = await useBaseFetch(
'user',
{
headers: {
Authorization: auth.token,
},
},
true,
)
} catch {
authCookie.value = null
}
}
return auth;
};
return auth
}
export const getAuthUrl = (provider, redirect = "/dashboard") => {
const config = useRuntimeConfig();
const route = useNativeRoute();
export const getAuthUrl = (provider, redirect = '/dashboard') => {
const config = useRuntimeConfig()
const route = useNativeRoute()
const fullURL = route.query.launcher
? "https://launcher-files.modrinth.com"
: `${config.public.siteUrl}/auth/sign-in?redirect=${redirect}`;
const fullURL = route.query.launcher
? 'https://launcher-files.modrinth.com'
: `${config.public.siteUrl}/auth/sign-in?redirect=${redirect}`
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`;
};
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`
}
export const removeAuthProvider = async (provider) => {
startLoading();
try {
const auth = await useAuth();
startLoading()
try {
const auth = await useAuth()
await useBaseFetch("auth/provider", {
method: "DELETE",
body: {
provider,
},
});
await useAuth(auth.value.token);
} catch (err) {
const { addNotification } = injectNotificationManager();
addNotification({
title: "An error occurred",
text: err.data.description,
type: "error",
});
}
stopLoading();
};
await useBaseFetch('auth/provider', {
method: 'DELETE',
body: {
provider,
},
})
await useAuth(auth.value.token)
} catch (err) {
const { addNotification } = injectNotificationManager()
addNotification({
title: 'An error occurred',
text: err.data.description,
type: 'error',
})
}
stopLoading()
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,26 @@
const formatters = new WeakMap<object, Intl.NumberFormat>();
const formatters = new WeakMap<object, Intl.NumberFormat>()
export function useCompactNumber(truncate = false, fractionDigits = 2, locale?: string) {
const context = {};
const context = {}
let formatter = formatters.get(context);
let formatter = formatters.get(context)
if (!formatter) {
formatter = new Intl.NumberFormat(locale, {
notation: "compact",
maximumFractionDigits: fractionDigits,
});
formatters.set(context, formatter);
}
if (!formatter) {
formatter = new Intl.NumberFormat(locale, {
notation: 'compact',
maximumFractionDigits: fractionDigits,
})
formatters.set(context, formatter)
}
function format(value: number): string {
let formattedValue = value;
if (truncate) {
const scale = Math.pow(10, fractionDigits);
formattedValue = Math.floor(value * scale) / scale;
}
return formatter!.format(formattedValue);
}
function format(value: number): string {
let formattedValue = value
if (truncate) {
const scale = Math.pow(10, fractionDigits)
formattedValue = Math.floor(value * scale) / scale
}
return formatter!.format(formattedValue)
}
return format;
return format
}

View File

@@ -1,37 +1,37 @@
import { useState, useRequestHeaders } from "#imports";
import { useRequestHeaders, useState } from '#imports'
export const useUserCountry = () => {
const country = useState<string>("userCountry", () => "US");
const fromServer = useState<boolean>("userCountryFromServer", () => false);
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.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;
// @ts-expect-error - ignore TS not knowing about navigator.userLanguage
const lang = navigator.language || navigator.userLanguage || "";
const region = lang.split("-")[1];
if (region) {
country.value = region.toUpperCase();
}
});
}
if (import.meta.client) {
onMounted(() => {
if (fromServer.value) return
// @ts-expect-error - ignore TS not knowing about navigator.userLanguage
const lang = navigator.language || navigator.userLanguage || ''
const region = lang.split('-')[1]
if (region) {
country.value = region.toUpperCase()
}
})
}
return country;
};
return country
}

View File

@@ -1,92 +1,90 @@
const safeTags = new Map<string, string>();
const safeTags = new Map<string, string>()
function safeTagFor(locale: string) {
let safeTag = safeTags.get(locale);
if (safeTag == null) {
safeTag = new Intl.Locale(locale).baseName;
safeTags.set(locale, safeTag);
}
return safeTag;
let safeTag = safeTags.get(locale)
if (safeTag == null) {
safeTag = new Intl.Locale(locale).baseName
safeTags.set(locale, safeTag)
}
return safeTag
}
type DisplayNamesWrapper = Intl.DisplayNames & {
of(tag: string): string | undefined;
};
of(tag: string): string | undefined
}
const displayNamesDicts = new Map<string, DisplayNamesWrapper>();
const displayNamesDicts = new Map<string, DisplayNamesWrapper>()
function getWrapperKey(locale: string, options: Intl.DisplayNamesOptions) {
return JSON.stringify({ ...options, locale });
return JSON.stringify({ ...options, locale })
}
export function createDisplayNames(
locale: string,
options: Intl.DisplayNamesOptions = { type: "language" },
locale: string,
options: Intl.DisplayNamesOptions = { type: 'language' },
) {
const wrapperKey = getWrapperKey(locale, options);
let wrapper = displayNamesDicts.get(wrapperKey);
const wrapperKey = getWrapperKey(locale, options)
let wrapper = displayNamesDicts.get(wrapperKey)
if (wrapper == null) {
const dict = new Intl.DisplayNames(locale, options);
if (wrapper == null) {
const dict = new Intl.DisplayNames(locale, options)
const badTags: string[] = [];
const badTags: string[] = []
wrapper = {
resolvedOptions() {
return dict.resolvedOptions();
},
of(tag: string) {
let attempt = 0;
wrapper = {
resolvedOptions() {
return dict.resolvedOptions()
},
of(tag: string) {
let attempt = 0
// eslint-disable-next-line no-labels
lookupLoop: do {
let lookup: string;
switch (attempt) {
case 0:
lookup = tag;
break;
case 1:
lookup = safeTagFor(tag);
break;
default:
// eslint-disable-next-line no-labels
break lookupLoop;
}
lookupLoop: do {
let lookup: string
switch (attempt) {
case 0:
lookup = tag
break
case 1:
lookup = safeTagFor(tag)
break
default:
break lookupLoop
}
if (badTags.includes(lookup)) continue;
if (badTags.includes(lookup)) continue
try {
return dict.of(lookup);
} catch {
console.warn(
`Failed to get display name for ${lookup} using dictionary for ${
this.resolvedOptions().locale
}`,
);
badTags.push(lookup);
continue;
}
} while (++attempt < 5);
try {
return dict.of(lookup)
} catch {
console.warn(
`Failed to get display name for ${lookup} using dictionary for ${
this.resolvedOptions().locale
}`,
)
badTags.push(lookup)
continue
}
} while (++attempt < 5)
return undefined;
},
};
return undefined
},
}
displayNamesDicts.set(wrapperKey, wrapper);
}
displayNamesDicts.set(wrapperKey, wrapper)
}
return wrapper;
return wrapper
}
export function useDisplayNames(
locale: string | (() => string) | Ref<string>,
options?:
| (Intl.DisplayNamesOptions | undefined)
| (() => Intl.DisplayNamesOptions | undefined)
| Ref<Intl.DisplayNamesOptions | undefined>,
locale: string | (() => string) | Ref<string>,
options?:
| (Intl.DisplayNamesOptions | undefined)
| (() => Intl.DisplayNamesOptions | undefined)
| Ref<Intl.DisplayNamesOptions | undefined>,
) {
const $locale = toRef(locale);
const $options = toRef(options);
const $locale = toRef(locale)
const $options = toRef(options)
return computed(() => createDisplayNames($locale.value, $options.value));
return computed(() => createDisplayNames($locale.value, $options.value))
}

View File

@@ -1,107 +1,107 @@
import type { CookieOptions } from "#app";
import type { CookieOptions } from '#app'
export type ProjectDisplayMode = "list" | "grid" | "gallery";
export type DarkColorTheme = "dark" | "oled" | "retro";
export type ProjectDisplayMode = 'list' | 'grid' | 'gallery'
export type DarkColorTheme = 'dark' | 'oled' | 'retro'
export interface NumberFlag {
min: number;
max: number;
min: number
max: number
}
export type BooleanFlag = boolean;
export type BooleanFlag = boolean
export type RadioFlag = ProjectDisplayMode | DarkColorTheme;
export type RadioFlag = ProjectDisplayMode | DarkColorTheme
export type FlagValue = BooleanFlag; /* | NumberFlag | RadioFlag */
export type FlagValue = BooleanFlag /* | NumberFlag | RadioFlag */
const validateValues = <K extends PropertyKey>(flags: Record<K, FlagValue>) => flags;
const validateValues = <K extends PropertyKey>(flags: Record<K, FlagValue>) => flags
export const DEFAULT_FEATURE_FLAGS = validateValues({
// Developer flags
developerMode: false,
showVersionFilesInTable: false,
showAdsWithPlus: false,
alwaysShowChecklistAsPopup: true,
// Developer flags
developerMode: false,
showVersionFilesInTable: false,
showAdsWithPlus: false,
alwaysShowChecklistAsPopup: true,
// Feature toggles
projectTypesPrimaryNav: false,
hidePlusPromoInUserMenu: false,
oldProjectCards: true,
newProjectCards: false,
projectBackground: false,
searchBackground: false,
advancedDebugInfo: false,
showProjectPageDownloadModalServersPromo: false,
showProjectPageCreateServersTooltip: true,
showProjectPageQuickServerButton: false,
// advancedRendering: true,
// externalLinksNewTab: true,
// notUsingBlockers: false,
// hideModrinthAppPromos: false,
// preferredDarkTheme: 'dark',
// hideStagingBanner: false,
// Feature toggles
projectTypesPrimaryNav: false,
hidePlusPromoInUserMenu: false,
oldProjectCards: true,
newProjectCards: false,
projectBackground: false,
searchBackground: false,
advancedDebugInfo: false,
showProjectPageDownloadModalServersPromo: false,
showProjectPageCreateServersTooltip: true,
showProjectPageQuickServerButton: false,
// advancedRendering: true,
// externalLinksNewTab: true,
// notUsingBlockers: false,
// hideModrinthAppPromos: false,
// preferredDarkTheme: 'dark',
// hideStagingBanner: false,
// Project display modes
// modSearchDisplayMode: 'list',
// pluginSearchDisplayMode: 'list',
// resourcePackSearchDisplayMode: 'gallery',
// modpackSearchDisplayMode: 'list',
// shaderSearchDisplayMode: 'gallery',
// dataPackSearchDisplayMode: 'list',
// userProjectDisplayMode: 'list',
// collectionProjectDisplayMode: 'list',
} as const);
// Project display modes
// modSearchDisplayMode: 'list',
// pluginSearchDisplayMode: 'list',
// resourcePackSearchDisplayMode: 'gallery',
// modpackSearchDisplayMode: 'list',
// shaderSearchDisplayMode: 'gallery',
// dataPackSearchDisplayMode: 'list',
// userProjectDisplayMode: 'list',
// collectionProjectDisplayMode: 'list',
} as const)
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS;
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
export type AllFeatureFlags = {
[key in FeatureFlag]: (typeof DEFAULT_FEATURE_FLAGS)[key];
};
[key in FeatureFlag]: (typeof DEFAULT_FEATURE_FLAGS)[key]
}
export type PartialFeatureFlags = Partial<AllFeatureFlags>;
export type PartialFeatureFlags = Partial<AllFeatureFlags>
const COOKIE_OPTIONS = {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: "lax",
secure: true,
httpOnly: false,
path: "/",
} satisfies CookieOptions<PartialFeatureFlags>;
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
secure: true,
httpOnly: false,
path: '/',
} satisfies CookieOptions<PartialFeatureFlags>
export const useFeatureFlags = () =>
useState<AllFeatureFlags>("featureFlags", () => {
const config = useRuntimeConfig();
useState<AllFeatureFlags>('featureFlags', () => {
const config = useRuntimeConfig()
const savedFlags = useCookie<PartialFeatureFlags>("featureFlags", COOKIE_OPTIONS);
const savedFlags = useCookie<PartialFeatureFlags>('featureFlags', COOKIE_OPTIONS)
if (!savedFlags.value) {
savedFlags.value = {};
}
if (!savedFlags.value) {
savedFlags.value = {}
}
const flags: AllFeatureFlags = JSON.parse(JSON.stringify(DEFAULT_FEATURE_FLAGS));
const flags: AllFeatureFlags = JSON.parse(JSON.stringify(DEFAULT_FEATURE_FLAGS))
const overrides = config.public.featureFlagOverrides as PartialFeatureFlags;
for (const key in overrides) {
if (key in flags) {
const flag = key as FeatureFlag;
const value = overrides[flag] as (typeof flags)[FeatureFlag];
flags[flag] = value;
}
}
const overrides = config.public.featureFlagOverrides as PartialFeatureFlags
for (const key in overrides) {
if (key in flags) {
const flag = key as FeatureFlag
const value = overrides[flag] as (typeof flags)[FeatureFlag]
flags[flag] = value
}
}
for (const key in savedFlags.value) {
if (key in flags) {
const flag = key as FeatureFlag;
const value = savedFlags.value[flag] as (typeof flags)[FeatureFlag];
flags[flag] = value;
}
}
for (const key in savedFlags.value) {
if (key in flags) {
const flag = key as FeatureFlag
const value = savedFlags.value[flag] as (typeof flags)[FeatureFlag]
flags[flag] = value
}
}
return flags;
});
return flags
})
export const saveFeatureFlags = () => {
const flags = useFeatureFlags();
const cookie = useCookie<PartialFeatureFlags>("featureFlags", COOKIE_OPTIONS);
cookie.value = flags.value;
};
const flags = useFeatureFlags()
const cookie = useCookie<PartialFeatureFlags>('featureFlags', COOKIE_OPTIONS)
cookie.value = flags.value
}

View File

@@ -1,36 +1,36 @@
export const useBaseFetch = async (url, options = {}, skipAuth = false) => {
const config = useRuntimeConfig();
let base = import.meta.server ? config.apiBaseUrl : config.public.apiBaseUrl;
const config = useRuntimeConfig()
let base = import.meta.server ? config.apiBaseUrl : config.public.apiBaseUrl
if (!options.headers) {
options.headers = {};
}
if (!options.headers) {
options.headers = {}
}
if (import.meta.server) {
options.headers["x-ratelimit-key"] = config.rateLimitKey;
}
if (import.meta.server) {
options.headers['x-ratelimit-key'] = config.rateLimitKey
}
if (!skipAuth) {
const auth = await useAuth();
if (!skipAuth) {
const auth = await useAuth()
options.headers.Authorization = auth.value.token;
}
options.headers.Authorization = auth.value.token
}
if (options.apiVersion || options.internal) {
// Base may end in /vD/ or /vD. We would need to replace the digit with the new version number
// and keep the trailing slash if it exists
const baseVersion = base.match(/\/v\d\//);
if (options.apiVersion || options.internal) {
// Base may end in /vD/ or /vD. We would need to replace the digit with the new version number
// and keep the trailing slash if it exists
const baseVersion = base.match(/\/v\d\//)
const replaceStr = options.internal ? `/_internal/` : `/v${options.apiVersion}/`;
const replaceStr = options.internal ? `/_internal/` : `/v${options.apiVersion}/`
if (baseVersion) {
base = base.replace(baseVersion[0], replaceStr);
} else {
base = base.replace(/\/v\d$/, replaceStr);
}
if (baseVersion) {
base = base.replace(baseVersion[0], replaceStr)
} else {
base = base.replace(/\/v\d$/, replaceStr)
}
delete options.apiVersion;
}
delete options.apiVersion
}
return await $fetch(`${base}${url}`, options);
};
return await $fetch(`${base}${url}`, options)
}

View File

@@ -1,46 +1,46 @@
type ImageUploadContext = {
projectID?: string;
context: "project" | "version" | "thread_message" | "report";
};
projectID?: string
context: 'project' | 'version' | 'thread_message' | 'report'
}
interface ImageUploadResponse {
id: string;
url: string;
id: string
url: string
}
export const useImageUpload = async (file: File, ctx: ImageUploadContext) => {
// Make sure file is of type image/png, image/jpeg, image/gif, or image/webp
if (
!file.type.startsWith("image/") ||
!["png", "jpeg", "gif", "webp"].includes(file.type.split("/")[1])
) {
throw new Error("File is not an accepted image type");
}
// Make sure file is of type image/png, image/jpeg, image/gif, or image/webp
if (
!file.type.startsWith('image/') ||
!['png', 'jpeg', 'gif', 'webp'].includes(file.type.split('/')[1])
) {
throw new Error('File is not an accepted image type')
}
// Make sure file is less than 1MB
if (file.size > 1024 * 1024) {
throw new Error("File exceeds the 1MiB size limit");
}
// Make sure file is less than 1MB
if (file.size > 1024 * 1024) {
throw new Error('File exceeds the 1MiB size limit')
}
const qs = new URLSearchParams();
if (ctx.projectID) qs.set("project_id", ctx.projectID);
qs.set("context", ctx.context);
qs.set("ext", file.type.split("/")[1]);
const url = `image?${qs.toString()}`;
const qs = new URLSearchParams()
if (ctx.projectID) qs.set('project_id', ctx.projectID)
qs.set('context', ctx.context)
qs.set('ext', file.type.split('/')[1])
const url = `image?${qs.toString()}`
const response = (await useBaseFetch(url, {
method: "POST",
body: file,
apiVersion: 3,
})) as ImageUploadResponse;
const response = (await useBaseFetch(url, {
method: 'POST',
body: file,
apiVersion: 3,
})) as ImageUploadResponse
// Type check to see if response has a url property and an id property
if (!response?.id || typeof response.id !== "string") {
throw new Error("Unexpected response from server");
}
if (!response?.url || typeof response.url !== "string") {
throw new Error("Unexpected response from server");
}
// Type check to see if response has a url property and an id property
if (!response?.id || typeof response.id !== 'string') {
throw new Error('Unexpected response from server')
}
if (!response?.url || typeof response.url !== 'string') {
throw new Error('Unexpected response from server')
}
return response;
};
return response
}

View File

@@ -1,13 +1,13 @@
export const useLoading = () => useState("loading", () => false);
export const useLoading = () => useState('loading', () => false)
export const startLoading = () => {
const loading = useLoading();
const loading = useLoading()
loading.value = true;
};
loading.value = true
}
export const stopLoading = () => {
const loading = useLoading();
const loading = useLoading()
loading.value = false;
};
loading.value = false
}

View File

@@ -1,7 +1,7 @@
export function useTheme() {
return useNuxtApp().$theme;
return useNuxtApp().$theme
}
export function useCosmetics() {
return useNuxtApp().$cosmetics;
return useNuxtApp().$cosmetics
}

View File

@@ -1 +1 @@
export { useRoute as useNativeRoute, useRouter as useNativeRouter } from "vue-router";
export { useRoute as useNativeRoute, useRouter as useNativeRouter } from 'vue-router'

View File

@@ -1,7 +1,7 @@
export const getArrayOrString = (x) => {
if (typeof x === "string" || x instanceof String) {
return [x];
} else {
return x;
}
};
if (typeof x === 'string' || x instanceof String) {
return [x]
} else {
return x
}
}

View File

@@ -4,7 +4,7 @@
* @param {string?} key The key of the route param to extract.
* @returns {import('vue').Ref<string | string[] | undefined>}
*/
export const useRouteId = (key = "id") => {
const route = useNativeRoute();
return route.params?.[key] || undefined;
};
export const useRouteId = (key = 'id') => {
const route = useNativeRoute()
return route.params?.[key] || undefined
}

View File

@@ -1,330 +1,330 @@
import type { JWTAuth, ModuleError, ModuleName } from "@modrinth/utils";
import { ModrinthServerError } from "@modrinth/utils";
import { injectNotificationManager } from "@modrinth/ui";
import { useServersFetch } from "./servers-fetch.ts";
import { injectNotificationManager } from '@modrinth/ui'
import type { JWTAuth, ModuleError, ModuleName } from '@modrinth/utils'
import { ModrinthServerError } from '@modrinth/utils'
import {
BackupsModule,
ContentModule,
FSModule,
GeneralModule,
NetworkModule,
StartupModule,
WSModule,
} from "./modules/index.ts";
BackupsModule,
ContentModule,
FSModule,
GeneralModule,
NetworkModule,
StartupModule,
WSModule,
} from './modules/index.ts'
import { useServersFetch } from './servers-fetch.ts'
export function handleError(err: any) {
const { addNotification } = injectNotificationManager();
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),
});
}
const { addNotification } = injectNotificationManager()
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<Record<ModuleName, ModuleError>> = {};
readonly serverId: string
private errors: Partial<Record<ModuleName, ModuleError>> = {}
readonly general: GeneralModule;
readonly content: ContentModule;
readonly backups: BackupsModule;
readonly network: NetworkModule;
readonly startup: StartupModule;
readonly ws: WSModule;
readonly fs: FSModule;
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;
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);
}
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<void> {
if (path.startsWith("/")) {
path = path.substring(1);
}
const folders = path.split("/");
let currentPath = "";
async createMissingFolders(path: string): Promise<void> {
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
}
}
}
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<any> {
return await useServersFetch(`servers/${this.serverId}/config/${fileName}`);
}
async fetchConfigFile(fileName: string): Promise<any> {
return await useServersFetch(`servers/${this.serverId}/config/${fileName}`)
}
constructServerProperties(properties: any): string {
let fileContent = `#Minecraft server properties\n#${new Date().toUTCString()}\n`;
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`;
}
}
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;
}
return fileContent
}
async processImage(iconUrl: string | undefined): Promise<string | undefined> {
const sharedImage = useState<string | undefined>(`server-icon-${this.serverId}`);
async processImage(iconUrl: string | undefined): Promise<string | undefined> {
const sharedImage = useState<string | undefined>(`server-icon-${this.serverId}`)
if (sharedImage.value) {
return sharedImage.value;
}
if (sharedImage.value) {
return sharedImage.value
}
try {
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`);
try {
const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, {
override: auth,
retry: 1, // Reduce retries for optional resources
});
try {
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`)
try {
const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, {
override: auth,
retry: 1, // Reduce retries for optional resources
})
if (fileData instanceof Blob && import.meta.client) {
const dataURL = await new Promise<string>((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) {
if (error.statusCode && error.statusCode >= 500) {
console.debug("Service unavailable, skipping icon processing");
sharedImage.value = undefined;
return undefined;
}
if (fileData instanceof Blob && import.meta.client) {
const dataURL = await new Promise<string>((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) {
if (error.statusCode && error.statusCode >= 500) {
console.debug('Service unavailable, skipping icon processing')
sharedImage.value = undefined
return undefined
}
if (error.statusCode === 404 && iconUrl) {
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 (error.statusCode === 404 && iconUrl) {
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<string>((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 (externalError: any) {
console.debug("Could not process external icon:", externalError.message);
}
}
} else {
throw error;
}
}
} catch (error: any) {
console.debug("Icon processing failed:", error.message);
}
if (import.meta.client) {
const dataURL = await new Promise<string>((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 (externalError: any) {
console.debug('Could not process external icon:', externalError.message)
}
}
} else {
throw error
}
}
} catch (error: any) {
console.debug('Icon processing failed:', error.message)
}
sharedImage.value = undefined;
return undefined;
}
sharedImage.value = undefined
return undefined
}
async testNodeReachability(): Promise<boolean> {
if (!this.general?.node?.instance) {
console.warn("No node instance available for ping test");
return false;
}
async testNodeReachability(): Promise<boolean> {
if (!this.general?.node?.instance) {
console.warn('No node instance available for ping test')
return false
}
const wsUrl = `wss://${this.general.node.instance}/pingtest`;
const wsUrl = `wss://${this.general.node.instance}/pingtest`
try {
return await new Promise((resolve) => {
const socket = new WebSocket(wsUrl);
const timeout = setTimeout(() => {
socket.close();
resolve(false);
}, 5000);
try {
return await new Promise((resolve) => {
const socket = new WebSocket(wsUrl)
const timeout = setTimeout(() => {
socket.close()
resolve(false)
}, 5000)
socket.onopen = () => {
clearTimeout(timeout);
socket.send(performance.now().toString());
};
socket.onopen = () => {
clearTimeout(timeout)
socket.send(performance.now().toString())
}
socket.onmessage = () => {
clearTimeout(timeout);
socket.close();
resolve(true);
};
socket.onmessage = () => {
clearTimeout(timeout)
socket.close()
resolve(true)
}
socket.onerror = () => {
clearTimeout(timeout);
resolve(false);
};
});
} catch (error) {
console.error(`Failed to ping node ${wsUrl}:`, error);
return false;
}
}
socket.onerror = () => {
clearTimeout(timeout)
resolve(false)
}
})
} catch (error) {
console.error(`Failed to ping node ${wsUrl}:`, error)
return false
}
}
async refresh(
modules: ModuleName[] = [],
options?: {
preserveConnection?: boolean;
preserveInstallState?: boolean;
},
): Promise<void> {
const modulesToRefresh =
modules.length > 0
? modules
: (["general", "content", "backups", "network", "startup", "ws", "fs"] as ModuleName[]);
async refresh(
modules: ModuleName[] = [],
options?: {
preserveConnection?: boolean
preserveInstallState?: boolean
},
): Promise<void> {
const modulesToRefresh =
modules.length > 0
? modules
: (['general', 'content', 'backups', 'network', 'startup', 'ws', 'fs'] as ModuleName[])
for (const module of modulesToRefresh) {
this.errors[module] = undefined;
for (const module of modulesToRefresh) {
this.errors[module] = undefined
try {
switch (module) {
case "general": {
if (options?.preserveConnection) {
const currentImage = this.general.image;
const currentMotd = this.general.motd;
const currentStatus = this.general.status;
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();
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) {
if (error instanceof ModrinthServerError) {
if (error.statusCode === 404 && ["fs", "content"].includes(module)) {
console.debug(`Optional ${module} resource not found:`, error.message);
continue;
}
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) {
if (error instanceof ModrinthServerError) {
if (error.statusCode === 404 && ['fs', 'content'].includes(module)) {
console.debug(`Optional ${module} resource not found:`, error.message)
continue
}
if (error.statusCode && error.statusCode >= 500) {
console.debug(`Temporary ${module} unavailable:`, error.message);
continue;
}
}
if (error.statusCode && error.statusCode >= 500) {
console.debug(`Temporary ${module} unavailable:`, error.message)
continue
}
}
this.errors[module] = {
error:
error instanceof ModrinthServerError
? error
: new ModrinthServerError("Unknown error", undefined, error as Error),
timestamp: Date.now(),
};
}
}
}
this.errors[module] = {
error:
error instanceof ModrinthServerError
? error
: new ModrinthServerError('Unknown error', undefined, error as Error),
timestamp: Date.now(),
}
}
}
}
get moduleErrors() {
return this.errors;
}
get moduleErrors() {
return this.errors
}
}
export const useModrinthServers = async (
serverId: string,
includedModules: ModuleName[] = ["general"],
serverId: string,
includedModules: ModuleName[] = ['general'],
) => {
const server = new ModrinthServer(serverId);
await server.refresh(includedModules);
return reactive(server);
};
const server = new ModrinthServer(serverId)
await server.refresh(includedModules)
return reactive(server)
}

View File

@@ -1,79 +1,80 @@
import type { Backup, AutoBackupSettings } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
import type { AutoBackupSettings, Backup } from '@modrinth/utils'
import { useServersFetch } from '../servers-fetch.ts'
import { ServerModule } from './base.ts'
export class BackupsModule extends ServerModule {
data: Backup[] = [];
data: Backup[] = []
async fetch(): Promise<void> {
this.data = await useServersFetch<Backup[]>(`servers/${this.serverId}/backups`, {}, "backups");
}
async fetch(): Promise<void> {
this.data = await useServersFetch<Backup[]>(`servers/${this.serverId}/backups`, {}, 'backups')
}
async create(backupName: string): Promise<string> {
const response = await useServersFetch<{ id: string }>(`servers/${this.serverId}/backups`, {
method: "POST",
body: { name: backupName },
});
await this.fetch(); // Refresh this module
return response.id;
}
async create(backupName: string): Promise<string> {
const response = await useServersFetch<{ id: string }>(`servers/${this.serverId}/backups`, {
method: 'POST',
body: { name: backupName },
})
await this.fetch() // Refresh this module
return response.id
}
async rename(backupId: string, newName: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/rename`, {
method: "POST",
body: { name: newName },
});
await this.fetch(); // Refresh this module
}
async rename(backupId: string, newName: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/rename`, {
method: 'POST',
body: { name: newName },
})
await this.fetch() // Refresh this module
}
async delete(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}`, {
method: "DELETE",
});
await this.fetch(); // Refresh this module
}
async delete(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}`, {
method: 'DELETE',
})
await this.fetch() // Refresh this module
}
async restore(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/restore`, {
method: "POST",
});
await this.fetch(); // Refresh this module
}
async restore(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/restore`, {
method: 'POST',
})
await this.fetch() // Refresh this module
}
async prepare(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/prepare-download`, {
method: "POST",
});
}
async prepare(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/prepare-download`, {
method: 'POST',
})
}
async lock(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/lock`, {
method: "POST",
});
await this.fetch(); // Refresh this module
}
async lock(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/lock`, {
method: 'POST',
})
await this.fetch() // Refresh this module
}
async unlock(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/unlock`, {
method: "POST",
});
await this.fetch(); // Refresh this module
}
async unlock(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/unlock`, {
method: 'POST',
})
await this.fetch() // Refresh this module
}
async retry(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/retry`, {
method: "POST",
});
}
async retry(backupId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/retry`, {
method: 'POST',
})
}
async updateAutoBackup(autoBackup: "enable" | "disable", interval: number): Promise<void> {
await useServersFetch(`servers/${this.serverId}/autobackup`, {
method: "POST",
body: { set: autoBackup, interval },
});
}
async updateAutoBackup(autoBackup: 'enable' | 'disable', interval: number): Promise<void> {
await useServersFetch(`servers/${this.serverId}/autobackup`, {
method: 'POST',
body: { set: autoBackup, interval },
})
}
async getAutoBackup(): Promise<AutoBackupSettings> {
return await useServersFetch(`servers/${this.serverId}/autobackup`);
}
async getAutoBackup(): Promise<AutoBackupSettings> {
return await useServersFetch(`servers/${this.serverId}/autobackup`)
}
}

View File

@@ -1,15 +1,15 @@
import type { ModrinthServer } from "../modrinth-servers.ts";
import type { ModrinthServer } from '../modrinth-servers.ts'
export abstract class ServerModule {
protected server: ModrinthServer;
protected server: ModrinthServer
constructor(server: ModrinthServer) {
this.server = server;
}
constructor(server: ModrinthServer) {
this.server = server
}
protected get serverId(): string {
return this.server.serverId;
}
protected get serverId(): string {
return this.server.serverId
}
abstract fetch(): Promise<void>;
abstract fetch(): Promise<void>
}

View File

@@ -1,36 +1,37 @@
import type { Mod, ContentType } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
import type { ContentType, Mod } from '@modrinth/utils'
import { useServersFetch } from '../servers-fetch.ts'
import { ServerModule } from './base.ts'
export class ContentModule extends ServerModule {
data: Mod[] = [];
data: Mod[] = []
async fetch(): Promise<void> {
const mods = await useServersFetch<Mod[]>(`servers/${this.serverId}/mods`, {}, "content");
this.data = mods.sort((a, b) => (a?.name ?? "").localeCompare(b?.name ?? ""));
}
async fetch(): Promise<void> {
const mods = await useServersFetch<Mod[]>(`servers/${this.serverId}/mods`, {}, 'content')
this.data = mods.sort((a, b) => (a?.name ?? '').localeCompare(b?.name ?? ''))
}
async install(contentType: ContentType, projectId: string, versionId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/mods`, {
method: "POST",
body: {
rinth_ids: { project_id: projectId, version_id: versionId },
install_as: contentType,
},
});
}
async install(contentType: ContentType, projectId: string, versionId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/mods`, {
method: 'POST',
body: {
rinth_ids: { project_id: projectId, version_id: versionId },
install_as: contentType,
},
})
}
async remove(path: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/deleteMod`, {
method: "POST",
body: { path },
});
}
async remove(path: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/deleteMod`, {
method: 'POST',
body: { path },
})
}
async reinstall(replace: string, projectId: string, versionId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/mods/update`, {
method: "POST",
body: { replace, project_id: projectId, version_id: versionId },
});
}
async reinstall(replace: string, projectId: string, versionId: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/mods/update`, {
method: 'POST',
body: { replace, project_id: projectId, version_id: versionId },
})
}
}

View File

@@ -1,247 +1,248 @@
import type {
FileUploadQuery,
JWTAuth,
DirectoryResponse,
FilesystemOp,
FSQueuedOp,
} from "@modrinth/utils";
import { ModrinthServerError } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
DirectoryResponse,
FilesystemOp,
FileUploadQuery,
FSQueuedOp,
JWTAuth,
} from '@modrinth/utils'
import { ModrinthServerError } from '@modrinth/utils'
import { useServersFetch } from '../servers-fetch.ts'
import { ServerModule } from './base.ts'
export class FSModule extends ServerModule {
auth!: JWTAuth;
ops: FilesystemOp[] = [];
queuedOps: FSQueuedOp[] = [];
opsQueuedForModification: string[] = [];
auth!: JWTAuth
ops: FilesystemOp[] = []
queuedOps: FSQueuedOp[] = []
opsQueuedForModification: string[] = []
async fetch(): Promise<void> {
this.auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`, {}, "fs");
this.ops = [];
this.queuedOps = [];
this.opsQueuedForModification = [];
}
async fetch(): Promise<void> {
this.auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`, {}, 'fs')
this.ops = []
this.queuedOps = []
this.opsQueuedForModification = []
}
private async retryWithAuth<T>(
requestFn: () => Promise<T>,
ignoreFailure: boolean = false,
): Promise<T> {
try {
return await requestFn();
} catch (error) {
if (error instanceof ModrinthServerError && error.statusCode === 401) {
console.debug("Auth failed, refreshing JWT and retrying");
await this.fetch(); // Refresh auth
return await requestFn();
}
private async retryWithAuth<T>(
requestFn: () => Promise<T>,
ignoreFailure: boolean = false,
): Promise<T> {
try {
return await requestFn()
} catch (error) {
if (error instanceof ModrinthServerError && error.statusCode === 401) {
console.debug('Auth failed, refreshing JWT and retrying')
await this.fetch() // Refresh auth
return await requestFn()
}
const available = await this.server.testNodeReachability();
if (!available && !ignoreFailure) {
this.server.moduleErrors.general = {
error: new ModrinthServerError(
"Unable to reach node. FS operation failed and subsequent ping test failed.",
500,
error as Error,
"fs",
),
timestamp: Date.now(),
};
}
const available = await this.server.testNodeReachability()
if (!available && !ignoreFailure) {
this.server.moduleErrors.general = {
error: new ModrinthServerError(
'Unable to reach node. FS operation failed and subsequent ping test failed.',
500,
error as Error,
'fs',
),
timestamp: Date.now(),
}
}
throw error;
}
}
throw error
}
}
listDirContents(
path: string,
page: number,
pageSize: number,
ignoreFailure: boolean = false,
): Promise<DirectoryResponse> {
return this.retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path);
return await useServersFetch(`/list?path=${encodedPath}&page=${page}&page_size=${pageSize}`, {
override: this.auth,
retry: false,
});
}, ignoreFailure);
}
listDirContents(
path: string,
page: number,
pageSize: number,
ignoreFailure: boolean = false,
): Promise<DirectoryResponse> {
return this.retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path)
return await useServersFetch(`/list?path=${encodedPath}&page=${page}&page_size=${pageSize}`, {
override: this.auth,
retry: false,
})
}, ignoreFailure)
}
createFileOrFolder(path: string, type: "file" | "directory"): Promise<void> {
return this.retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path);
await useServersFetch(`/create?path=${encodedPath}&type=${type}`, {
method: "POST",
contentType: "application/octet-stream",
override: this.auth,
});
});
}
createFileOrFolder(path: string, type: 'file' | 'directory'): Promise<void> {
return this.retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path)
await useServersFetch(`/create?path=${encodedPath}&type=${type}`, {
method: 'POST',
contentType: 'application/octet-stream',
override: this.auth,
})
})
}
uploadFile(path: string, file: File): FileUploadQuery {
const encodedPath = encodeURIComponent(path);
const progressSubject = new EventTarget();
const abortController = new AbortController();
uploadFile(path: string, file: File): FileUploadQuery {
const encodedPath = encodeURIComponent(path)
const progressSubject = new EventTarget()
const abortController = new AbortController()
const uploadPromise = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
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.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.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.onerror = () => reject(new Error('Upload failed'))
xhr.onabort = () => reject(new Error('Upload cancelled'))
xhr.open("POST", `https://${this.auth.url}/create?path=${encodedPath}&type=file`);
xhr.setRequestHeader("Authorization", `Bearer ${this.auth.token}`);
xhr.setRequestHeader("Content-Type", "application/octet-stream");
xhr.send(file);
xhr.open('POST', `https://${this.auth.url}/create?path=${encodedPath}&type=file`)
xhr.setRequestHeader('Authorization', `Bearer ${this.auth.token}`)
xhr.setRequestHeader('Content-Type', 'application/octet-stream')
xhr.send(file)
abortController.signal.addEventListener("abort", () => xhr.abort());
});
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(),
} as FileUploadQuery;
}
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(),
} as FileUploadQuery
}
renameFileOrFolder(path: string, name: string): Promise<void> {
const pathName = path.split("/").slice(0, -1).join("/") + "/" + name;
return this.retryWithAuth(async () => {
await useServersFetch(`/move`, {
method: "POST",
override: this.auth,
body: { source: path, destination: pathName },
});
});
}
renameFileOrFolder(path: string, name: string): Promise<void> {
const pathName = path.split('/').slice(0, -1).join('/') + '/' + name
return this.retryWithAuth(async () => {
await useServersFetch(`/move`, {
method: 'POST',
override: this.auth,
body: { source: path, destination: pathName },
})
})
}
updateFile(path: string, content: string): Promise<void> {
const octetStream = new Blob([content], { type: "application/octet-stream" });
return this.retryWithAuth(async () => {
await useServersFetch(`/update?path=${path}`, {
method: "PUT",
contentType: "application/octet-stream",
body: octetStream,
override: this.auth,
});
});
}
updateFile(path: string, content: string): Promise<void> {
const octetStream = new Blob([content], { type: 'application/octet-stream' })
return this.retryWithAuth(async () => {
await useServersFetch(`/update?path=${path}`, {
method: 'PUT',
contentType: 'application/octet-stream',
body: octetStream,
override: this.auth,
})
})
}
moveFileOrFolder(path: string, newPath: string): Promise<void> {
return this.retryWithAuth(async () => {
await this.server.createMissingFolders(newPath.substring(0, newPath.lastIndexOf("/")));
await useServersFetch(`/move`, {
method: "POST",
override: this.auth,
body: { source: path, destination: newPath },
});
});
}
moveFileOrFolder(path: string, newPath: string): Promise<void> {
return this.retryWithAuth(async () => {
await this.server.createMissingFolders(newPath.substring(0, newPath.lastIndexOf('/')))
await useServersFetch(`/move`, {
method: 'POST',
override: this.auth,
body: { source: path, destination: newPath },
})
})
}
deleteFileOrFolder(path: string, recursive: boolean): Promise<void> {
const encodedPath = encodeURIComponent(path);
return this.retryWithAuth(async () => {
await useServersFetch(`/delete?path=${encodedPath}&recursive=${recursive}`, {
method: "DELETE",
override: this.auth,
});
});
}
deleteFileOrFolder(path: string, recursive: boolean): Promise<void> {
const encodedPath = encodeURIComponent(path)
return this.retryWithAuth(async () => {
await useServersFetch(`/delete?path=${encodedPath}&recursive=${recursive}`, {
method: 'DELETE',
override: this.auth,
})
})
}
downloadFile(path: string, raw: boolean = false, ignoreFailure: boolean = false): Promise<any> {
return this.retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path);
const fileData = await useServersFetch(`/download?path=${encodedPath}`, {
override: this.auth,
});
downloadFile(path: string, raw: boolean = false, ignoreFailure: boolean = false): Promise<any> {
return this.retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path)
const fileData = await useServersFetch(`/download?path=${encodedPath}`, {
override: this.auth,
})
if (fileData instanceof Blob) {
return raw ? fileData : await fileData.text();
}
return fileData;
}, ignoreFailure);
}
if (fileData instanceof Blob) {
return raw ? fileData : await fileData.text()
}
return fileData
}, ignoreFailure)
}
extractFile(
path: string,
override = true,
dry = false,
silentQueue = false,
): Promise<{ modpack_name: string | null; conflicting_files: string[] }> {
return this.retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path);
extractFile(
path: string,
override = true,
dry = false,
silentQueue = false,
): Promise<{ modpack_name: string | null; conflicting_files: string[] }> {
return this.retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path)
if (!silentQueue) {
this.queuedOps.push({ op: "unarchive", src: path });
setTimeout(() => this.removeQueuedOp("unarchive", path), 4000);
}
if (!silentQueue) {
this.queuedOps.push({ op: 'unarchive', src: path })
setTimeout(() => this.removeQueuedOp('unarchive', path), 4000)
}
try {
return await useServersFetch(
`/unarchive?src=${encodedPath}&trg=/&override=${override}&dry=${dry}`,
{
method: "POST",
override: this.auth,
version: 1,
},
undefined,
"Error extracting file",
);
} catch (err) {
this.removeQueuedOp("unarchive", path);
throw err;
}
});
}
try {
return await useServersFetch(
`/unarchive?src=${encodedPath}&trg=/&override=${override}&dry=${dry}`,
{
method: 'POST',
override: this.auth,
version: 1,
},
undefined,
'Error extracting file',
)
} catch (err) {
this.removeQueuedOp('unarchive', path)
throw err
}
})
}
modifyOp(id: string, action: "dismiss" | "cancel"): Promise<void> {
return this.retryWithAuth(async () => {
await useServersFetch(
`/ops/${action}?id=${id}`,
{
method: "POST",
override: this.auth,
version: 1,
},
undefined,
`Error ${action === "dismiss" ? "dismissing" : "cancelling"} filesystem operation`,
);
modifyOp(id: string, action: 'dismiss' | 'cancel'): Promise<void> {
return this.retryWithAuth(async () => {
await useServersFetch(
`/ops/${action}?id=${id}`,
{
method: 'POST',
override: this.auth,
version: 1,
},
undefined,
`Error ${action === 'dismiss' ? 'dismissing' : 'cancelling'} filesystem operation`,
)
this.opsQueuedForModification = this.opsQueuedForModification.filter((x: string) => x !== id);
this.ops = this.ops.filter((x: FilesystemOp) => x.id !== id);
});
}
this.opsQueuedForModification = this.opsQueuedForModification.filter((x: string) => x !== id)
this.ops = this.ops.filter((x: FilesystemOp) => x.id !== id)
})
}
removeQueuedOp(op: FSQueuedOp["op"], src: string): void {
this.queuedOps = this.queuedOps.filter((x: FSQueuedOp) => x.op !== op || x.src !== src);
}
removeQueuedOp(op: FSQueuedOp['op'], src: string): void {
this.queuedOps = this.queuedOps.filter((x: FSQueuedOp) => x.op !== op || x.src !== src)
}
clearQueuedOps(): void {
this.queuedOps = [];
}
clearQueuedOps(): void {
this.queuedOps = []
}
}

View File

@@ -1,224 +1,229 @@
import { $fetch } from "ofetch";
import type { ServerGeneral, Project, PowerAction, JWTAuth } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
import type { JWTAuth, PowerAction, Project, ServerGeneral } from '@modrinth/utils'
import { $fetch } from 'ofetch'
import { useServersFetch } from '../servers-fetch.ts'
import { ServerModule } from './base.ts'
export class GeneralModule extends ServerModule implements ServerGeneral {
server_id!: string;
name!: string;
owner_id!: string;
net!: { ip: string; port: number; domain: string };
game!: string;
backup_quota!: number;
used_backup_quota!: number;
status!: string;
suspension_reason!: string;
loader!: string;
loader_version!: string;
mc_version!: string;
upstream!: {
kind: "modpack" | "mod" | "resourcepack";
version_id: string;
project_id: string;
} | null;
server_id!: string
name!: string
owner_id!: string
net!: { ip: string; port: number; domain: string }
game!: string
backup_quota!: number
used_backup_quota!: number
status!: string
suspension_reason!: 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;
notices?: any[];
node!: { token: string; instance: string };
flows?: { intro?: boolean };
motd?: string
image?: string
project?: Project
sftp_username!: string
sftp_password!: string
sftp_host!: string
datacenter?: string
notices?: any[]
node!: { token: string; instance: string }
flows?: { intro?: boolean }
async fetch(): Promise<void> {
const data = await useServersFetch<ServerGeneral>(`servers/${this.serverId}`, {}, "general");
async fetch(): Promise<void> {
const data = await useServersFetch<ServerGeneral>(`servers/${this.serverId}`, {}, 'general')
if (data.upstream?.project_id) {
const project = await $fetch(
`https://api.modrinth.com/v2/project/${data.upstream.project_id}`,
);
data.project = project as Project;
}
if (data.upstream?.project_id) {
const project = await $fetch(
`https://api.modrinth.com/v2/project/${data.upstream.project_id}`,
)
data.project = project as Project
}
if (import.meta.client) {
data.image = (await this.server.processImage(data.project?.icon_url)) ?? undefined;
}
if (import.meta.client) {
data.image = (await this.server.processImage(data.project?.icon_url)) ?? undefined
}
try {
const motd = await this.getMotd();
if (motd === "A Minecraft Server") {
await this.setMotd(
`§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`,
);
}
data.motd = motd;
} catch {
console.error("[Modrinth Servers] [General] Failed to fetch MOTD.");
data.motd = undefined;
}
try {
const motd = await this.getMotd()
if (motd === 'A Minecraft Server') {
await this.setMotd(
`§b${data.project?.title || data.loader + ' ' + data.mc_version} §f♦ §aModrinth Servers`,
)
}
data.motd = motd
} catch {
console.error('[Modrinth Servers] [General] Failed to fetch MOTD.')
data.motd = undefined
}
// Copy data to this module
Object.assign(this, data);
}
// Copy data to this module
Object.assign(this, data)
}
async updateName(newName: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/name`, {
method: "POST",
body: { name: newName },
});
}
async updateName(newName: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/name`, {
method: 'POST',
body: { name: newName },
})
}
async power(action: PowerAction): Promise<void> {
await useServersFetch(`servers/${this.serverId}/power`, {
method: "POST",
body: { action },
});
await new Promise((resolve) => setTimeout(resolve, 1000));
await this.fetch(); // Refresh this module
}
async power(action: PowerAction): Promise<void> {
await useServersFetch(`servers/${this.serverId}/power`, {
method: 'POST',
body: { action },
})
await new Promise((resolve) => setTimeout(resolve, 1000))
await this.fetch() // Refresh this module
}
async reinstall(
loader: boolean,
projectId: string,
versionId?: string,
loaderVersionId?: string,
hardReset: boolean = false,
): Promise<void> {
const hardResetParam = hardReset ? "true" : "false";
if (loader) {
if (projectId.toLowerCase() === "neoforge") {
projectId = "NeoForge";
}
await useServersFetch(`servers/${this.serverId}/reinstall?hard=${hardResetParam}`, {
method: "POST",
body: { loader: projectId, loader_version: loaderVersionId, game_version: versionId },
});
} else {
await useServersFetch(`servers/${this.serverId}/reinstall?hard=${hardResetParam}`, {
method: "POST",
body: { project_id: projectId, version_id: versionId },
});
}
}
async reinstall(
loader: boolean,
projectId: string,
versionId?: string,
loaderVersionId?: string,
hardReset: boolean = false,
): Promise<void> {
const hardResetParam = hardReset ? 'true' : 'false'
if (loader) {
if (projectId.toLowerCase() === 'neoforge') {
projectId = 'NeoForge'
}
await useServersFetch(`servers/${this.serverId}/reinstall?hard=${hardResetParam}`, {
method: 'POST',
body: {
loader: projectId,
loader_version: loaderVersionId,
game_version: versionId,
},
})
} else {
await useServersFetch(`servers/${this.serverId}/reinstall?hard=${hardResetParam}`, {
method: 'POST',
body: { project_id: projectId, version_id: versionId },
})
}
}
reinstallFromMrpack(
mrpack: File,
hardReset: boolean = false,
): {
promise: Promise<void>;
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) => void;
} {
const hardResetParam = hardReset ? "true" : "false";
reinstallFromMrpack(
mrpack: File,
hardReset: boolean = false,
): {
promise: Promise<void>
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) => void
} {
const hardResetParam = hardReset ? 'true' : 'false'
const progressSubject = new EventTarget();
const progressSubject = new EventTarget()
const uploadPromise = (async () => {
try {
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/reinstallFromMrpack`);
const uploadPromise = (async () => {
try {
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/reinstallFromMrpack`)
await new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest();
await new Promise<void>((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.upload.addEventListener("progress", (e) => {
if (e.lengthComputable) {
progressSubject.dispatchEvent(
new CustomEvent("progress", {
detail: {
loaded: e.loaded,
total: e.total,
progress: (e.loaded / e.total) * 100,
},
}),
);
}
});
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
progressSubject.dispatchEvent(
new CustomEvent('progress', {
detail: {
loaded: e.loaded,
total: e.total,
progress: (e.loaded / e.total) * 100,
},
}),
)
}
})
xhr.onload = () =>
xhr.status >= 200 && xhr.status < 300
? resolve()
: reject(new Error(`[pyroservers] XHR error status: ${xhr.status}`));
xhr.onload = () =>
xhr.status >= 200 && xhr.status < 300
? resolve()
: reject(new Error(`[pyroservers] XHR error status: ${xhr.status}`))
xhr.onerror = () => reject(new Error("[pyroservers] .mrpack upload failed"));
xhr.onabort = () => reject(new Error("[pyroservers] .mrpack upload cancelled"));
xhr.ontimeout = () => reject(new Error("[pyroservers] .mrpack upload timed out"));
xhr.timeout = 30 * 60 * 1000;
xhr.onerror = () => reject(new Error('[pyroservers] .mrpack upload failed'))
xhr.onabort = () => reject(new Error('[pyroservers] .mrpack upload cancelled'))
xhr.ontimeout = () => reject(new Error('[pyroservers] .mrpack upload timed out'))
xhr.timeout = 30 * 60 * 1000
xhr.open("POST", `https://${auth.url}/reinstallMrpackMultiparted?hard=${hardResetParam}`);
xhr.setRequestHeader("Authorization", `Bearer ${auth.token}`);
xhr.open('POST', `https://${auth.url}/reinstallMrpackMultiparted?hard=${hardResetParam}`)
xhr.setRequestHeader('Authorization', `Bearer ${auth.token}`)
const formData = new FormData();
formData.append("file", mrpack);
xhr.send(formData);
});
} catch (err) {
console.error("Error reinstalling from mrpack:", err);
throw err;
}
})();
const formData = new FormData()
formData.append('file', mrpack)
xhr.send(formData)
})
} catch (err) {
console.error('Error reinstalling from mrpack:', err)
throw err
}
})()
return {
promise: uploadPromise,
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) =>
progressSubject.addEventListener("progress", ((e: CustomEvent) =>
cb(e.detail)) as EventListener),
};
}
return {
promise: uploadPromise,
onProgress: (cb: (p: { loaded: number; total: number; progress: number }) => void) =>
progressSubject.addEventListener('progress', ((e: CustomEvent) =>
cb(e.detail)) as EventListener),
}
}
async suspend(status: boolean): Promise<void> {
await useServersFetch(`servers/${this.serverId}/suspend`, {
method: "POST",
body: { suspended: status },
});
}
async suspend(status: boolean): Promise<void> {
await useServersFetch(`servers/${this.serverId}/suspend`, {
method: 'POST',
body: { suspended: status },
})
}
async endIntro(): Promise<void> {
await useServersFetch(`servers/${this.serverId}/flows/intro`, {
method: "DELETE",
version: 1,
});
await this.fetch(); // Refresh this module
}
async endIntro(): Promise<void> {
await useServersFetch(`servers/${this.serverId}/flows/intro`, {
method: 'DELETE',
version: 1,
})
await this.fetch() // Refresh this module
}
async getMotd(): Promise<string | undefined> {
try {
const props = await this.server.fs.downloadFile("/server.properties", false, true);
if (props) {
const lines = props.split("\n");
for (const line of lines) {
if (line.startsWith("motd=")) {
return line.slice(5);
}
}
}
} catch {
return undefined;
}
return undefined;
}
async getMotd(): Promise<string | undefined> {
try {
const props = await this.server.fs.downloadFile('/server.properties', false, true)
if (props) {
const lines = props.split('\n')
for (const line of lines) {
if (line.startsWith('motd=')) {
return line.slice(5)
}
}
}
} catch {
return undefined
}
return undefined
}
async setMotd(motd: string): Promise<void> {
try {
const props = (await this.server.fetchConfigFile("ServerProperties")) as any;
if (props) {
props.motd = motd;
const newProps = this.server.constructServerProperties(props);
const octetStream = new Blob([newProps], { type: "application/octet-stream" });
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`);
async setMotd(motd: string): Promise<void> {
try {
const props = (await this.server.fetchConfigFile('ServerProperties')) as any
if (props) {
props.motd = motd
const newProps = this.server.constructServerProperties(props)
const octetStream = new Blob([newProps], { type: 'application/octet-stream' })
const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`)
await useServersFetch(`/update?path=/server.properties`, {
method: "PUT",
contentType: "application/octet-stream",
body: octetStream,
override: auth,
});
}
} catch {
console.error(
"[Modrinth Servers] [General] Failed to set MOTD due to lack of server properties file.",
);
}
}
await useServersFetch(`/update?path=/server.properties`, {
method: 'PUT',
contentType: 'application/octet-stream',
body: octetStream,
override: auth,
})
}
} catch {
console.error(
'[Modrinth Servers] [General] Failed to set MOTD due to lack of server properties file.',
)
}
}
}

View File

@@ -1,8 +1,8 @@
export * from "./base.ts";
export * from "./backups.ts";
export * from "./content.ts";
export * from "./fs.ts";
export * from "./general.ts";
export * from "./network.ts";
export * from "./startup.ts";
export * from "./ws.ts";
export * from './backups.ts'
export * from './base.ts'
export * from './content.ts'
export * from './fs.ts'
export * from './general.ts'
export * from './network.ts'
export * from './startup.ts'
export * from './ws.ts'

View File

@@ -1,47 +1,48 @@
import type { Allocation } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
import type { Allocation } from '@modrinth/utils'
import { useServersFetch } from '../servers-fetch.ts'
import { ServerModule } from './base.ts'
export class NetworkModule extends ServerModule {
allocations: Allocation[] = [];
allocations: Allocation[] = []
async fetch(): Promise<void> {
this.allocations = await useServersFetch<Allocation[]>(
`servers/${this.serverId}/allocations`,
{},
"network",
);
}
async fetch(): Promise<void> {
this.allocations = await useServersFetch<Allocation[]>(
`servers/${this.serverId}/allocations`,
{},
'network',
)
}
async reserveAllocation(name: string): Promise<Allocation> {
return await useServersFetch<Allocation>(`servers/${this.serverId}/allocations?name=${name}`, {
method: "POST",
});
}
async reserveAllocation(name: string): Promise<Allocation> {
return await useServersFetch<Allocation>(`servers/${this.serverId}/allocations?name=${name}`, {
method: 'POST',
})
}
async updateAllocation(port: number, name: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/allocations/${port}?name=${name}`, {
method: "PUT",
});
}
async updateAllocation(port: number, name: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/allocations/${port}?name=${name}`, {
method: 'PUT',
})
}
async deleteAllocation(port: number): Promise<void> {
await useServersFetch(`servers/${this.serverId}/allocations/${port}`, {
method: "DELETE",
});
}
async deleteAllocation(port: number): Promise<void> {
await useServersFetch(`servers/${this.serverId}/allocations/${port}`, {
method: 'DELETE',
})
}
async checkSubdomainAvailability(subdomain: string): Promise<boolean> {
const result = (await useServersFetch(`subdomains/${subdomain}/isavailable`)) as {
available: boolean;
};
return result.available;
}
async checkSubdomainAvailability(subdomain: string): Promise<boolean> {
const result = (await useServersFetch(`subdomains/${subdomain}/isavailable`)) as {
available: boolean
}
return result.available
}
async changeSubdomain(subdomain: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/subdomain`, {
method: "POST",
body: { subdomain },
});
}
async changeSubdomain(subdomain: string): Promise<void> {
await useServersFetch(`servers/${this.serverId}/subdomain`, {
method: 'POST',
body: { subdomain },
})
}
}

View File

@@ -1,26 +1,27 @@
import type { Startup, JDKVersion, JDKBuild } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
import type { JDKBuild, JDKVersion, Startup } from '@modrinth/utils'
import { useServersFetch } from '../servers-fetch.ts'
import { ServerModule } from './base.ts'
export class StartupModule extends ServerModule implements Startup {
invocation!: string;
original_invocation!: string;
jdk_version!: JDKVersion;
jdk_build!: JDKBuild;
invocation!: string
original_invocation!: string
jdk_version!: JDKVersion
jdk_build!: JDKBuild
async fetch(): Promise<void> {
const data = await useServersFetch<Startup>(`servers/${this.serverId}/startup`, {}, "startup");
Object.assign(this, data);
}
async fetch(): Promise<void> {
const data = await useServersFetch<Startup>(`servers/${this.serverId}/startup`, {}, 'startup')
Object.assign(this, data)
}
async update(invocation: string, jdkVersion: JDKVersion, jdkBuild: JDKBuild): Promise<void> {
await useServersFetch(`servers/${this.serverId}/startup`, {
method: "POST",
body: {
invocation: invocation || null,
jdk_version: jdkVersion || null,
jdk_build: jdkBuild || null,
},
});
}
async update(invocation: string, jdkVersion: JDKVersion, jdkBuild: JDKBuild): Promise<void> {
await useServersFetch(`servers/${this.serverId}/startup`, {
method: 'POST',
body: {
invocation: invocation || null,
jdk_version: jdkVersion || null,
jdk_build: jdkBuild || null,
},
})
}
}

View File

@@ -1,13 +1,14 @@
import type { JWTAuth } from "@modrinth/utils";
import { useServersFetch } from "../servers-fetch.ts";
import { ServerModule } from "./base.ts";
import type { JWTAuth } from '@modrinth/utils'
import { useServersFetch } from '../servers-fetch.ts'
import { ServerModule } from './base.ts'
export class WSModule extends ServerModule implements JWTAuth {
url!: string;
token!: string;
url!: string
token!: string
async fetch(): Promise<void> {
const data = await useServersFetch<JWTAuth>(`servers/${this.serverId}/ws`, {}, "ws");
Object.assign(this, data);
}
async fetch(): Promise<void> {
const data = await useServersFetch<JWTAuth>(`servers/${this.serverId}/ws`, {}, 'ws')
Object.assign(this, data)
}
}

View File

@@ -1,222 +1,222 @@
import { $fetch, FetchError } from "ofetch";
import { ModrinthServerError, ModrinthServersFetchError } from "@modrinth/utils";
import type { V1ErrorInfo } from "@modrinth/utils";
import type { V1ErrorInfo } from '@modrinth/utils'
import { ModrinthServerError, ModrinthServersFetchError } from '@modrinth/utils'
import { $fetch, FetchError } from 'ofetch'
export interface ServersFetchOptions {
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
contentType?: string;
body?: Record<string, any>;
version?: number;
override?: {
url?: string;
token?: string;
};
retry?: number | boolean;
bypassAuth?: boolean;
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
contentType?: string
body?: Record<string, any>
version?: number
override?: {
url?: string
token?: string
}
retry?: number | boolean
bypassAuth?: boolean
}
export async function useServersFetch<T>(
path: string,
options: ServersFetchOptions = {},
module?: string,
errorContext?: string,
path: string,
options: ServersFetchOptions = {},
module?: string,
errorContext?: string,
): Promise<T> {
const config = useRuntimeConfig();
const auth = await useAuth();
const authToken = auth.value?.token;
const config = useRuntimeConfig()
const auth = await useAuth()
const authToken = auth.value?.token
if (!authToken && !options.bypassAuth) {
const error = new ModrinthServersFetchError(
"[Modrinth Servers] Cannot fetch without auth",
10000,
);
throw new ModrinthServerError("Missing auth token", 401, error, module);
}
if (!authToken && !options.bypassAuth) {
const error = new ModrinthServersFetchError(
'[Modrinth Servers] Cannot fetch without auth',
10000,
)
throw new ModrinthServerError('Missing auth token', 401, error, module)
}
const {
method = "GET",
contentType = "application/json",
body,
version = 0,
override,
retry = method === "GET" ? 3 : 0,
} = options;
const {
method = 'GET',
contentType = 'application/json',
body,
version = 0,
override,
retry = method === 'GET' ? 3 : 0,
} = options
const circuitBreakerKey = `${module || "default"}_${path}`;
const failureCount = useState<number>(`fetch_failures_${circuitBreakerKey}`, () => 0);
const lastFailureTime = useState<number>(`last_failure_${circuitBreakerKey}`, () => 0);
const circuitBreakerKey = `${module || 'default'}_${path}`
const failureCount = useState<number>(`fetch_failures_${circuitBreakerKey}`, () => 0)
const lastFailureTime = useState<number>(`last_failure_${circuitBreakerKey}`, () => 0)
const now = Date.now();
if (failureCount.value >= 3 && now - lastFailureTime.value < 30000) {
const error = new ModrinthServersFetchError(
"[Modrinth Servers] Circuit breaker open - too many recent failures",
503,
);
throw new ModrinthServerError("Service temporarily unavailable", 503, error, module);
}
const now = Date.now()
if (failureCount.value >= 3 && now - lastFailureTime.value < 30000) {
const error = new ModrinthServersFetchError(
'[Modrinth Servers] Circuit breaker open - too many recent failures',
503,
)
throw new ModrinthServerError('Service temporarily unavailable', 503, error, module)
}
if (now - lastFailureTime.value > 30000) {
failureCount.value = 0;
}
if (now - lastFailureTime.value > 30000) {
failureCount.value = 0
}
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
/\/$/,
"",
);
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
/\/$/,
'',
)
if (!base) {
const error = new ModrinthServersFetchError(
"[Modrinth Servers] Cannot fetch without base url. Make sure to set a PYRO_BASE_URL in environment variables",
10001,
);
throw new ModrinthServerError("Configuration error: Missing PYRO_BASE_URL", 500, error, module);
}
if (!base) {
const error = new ModrinthServersFetchError(
'[Modrinth Servers] Cannot fetch without base url. Make sure to set a PYRO_BASE_URL in environment variables',
10001,
)
throw new ModrinthServerError('Configuration error: Missing PYRO_BASE_URL', 500, error, module)
}
const versionString = `v${version}`;
let newOverrideUrl = override?.url;
if (newOverrideUrl && newOverrideUrl.includes("v0") && version !== 0) {
newOverrideUrl = newOverrideUrl.replace("v0", versionString);
}
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(/^\//, "")}`
: version === 0
? `${base}/modrinth/v${version}/${path.replace(/^\//, "")}`
: `${base}/v${version}/${path.replace(/^\//, "")}`;
const fullUrl = newOverrideUrl
? `https://${newOverrideUrl}/${path.replace(/^\//, '')}`
: version === 0
? `${base}/modrinth/v${version}/${path.replace(/^\//, '')}`
: `${base}/v${version}/${path.replace(/^\//, '')}`
const headers: Record<string, string> = {
"User-Agent": "Modrinth/1.0 (https://modrinth.com)",
"X-Archon-Request": "true",
Vary: "Accept, Origin",
};
const headers: Record<string, string> = {
'User-Agent': 'Modrinth/1.0 (https://modrinth.com)',
'X-Archon-Request': 'true',
Vary: 'Accept, Origin',
}
if (!options.bypassAuth) {
headers.Authorization = `Bearer ${override?.token ?? authToken}`;
headers["Access-Control-Allow-Headers"] = "Authorization";
}
if (!options.bypassAuth) {
headers.Authorization = `Bearer ${override?.token ?? authToken}`
headers['Access-Control-Allow-Headers'] = 'Authorization'
}
if (contentType !== "none") {
headers["Content-Type"] = contentType;
}
if (contentType !== 'none') {
headers['Content-Type'] = contentType
}
if (import.meta.client && typeof window !== "undefined") {
headers.Origin = window.location.origin;
}
if (import.meta.client && typeof window !== 'undefined') {
headers.Origin = window.location.origin
}
let attempts = 0;
const maxAttempts = (typeof retry === "boolean" ? (retry ? 3 : 1) : retry) + 1;
let lastError: Error | null = null;
let attempts = 0
const maxAttempts = (typeof retry === 'boolean' ? (retry ? 3 : 1) : retry) + 1
let lastError: Error | null = null
while (attempts < maxAttempts) {
try {
const response = await $fetch<T>(fullUrl, {
method,
headers,
body:
body && contentType === "application/json" ? JSON.stringify(body) : (body ?? undefined),
timeout: 10000,
});
while (attempts < maxAttempts) {
try {
const response = await $fetch<T>(fullUrl, {
method,
headers,
body:
body && contentType === 'application/json' ? JSON.stringify(body) : (body ?? undefined),
timeout: 10000,
})
failureCount.value = 0;
return response;
} catch (error) {
lastError = error as Error;
attempts++;
failureCount.value = 0
return response
} catch (error) {
lastError = error as Error
attempts++
if (error instanceof FetchError) {
const statusCode = error.response?.status;
const statusText = error.response?.statusText || "Unknown error";
if (error instanceof FetchError) {
const statusCode = error.response?.status
const statusText = error.response?.statusText || 'Unknown error'
if (statusCode && statusCode >= 500) {
failureCount.value++;
lastFailureTime.value = now;
}
if (statusCode && statusCode >= 500) {
failureCount.value++
lastFailureTime.value = now
}
let v1Error: V1ErrorInfo | undefined;
if (error.data?.error && error.data?.description) {
v1Error = {
context: errorContext,
...error.data,
};
}
let v1Error: V1ErrorInfo | undefined
if (error.data?.error && error.data?.description) {
v1Error = {
context: errorContext,
...error.data,
}
}
const errorMessages: { [key: number]: string } = {
400: "Bad Request",
401: "Unauthorized",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
408: "Request Timeout",
429: "You're making requests too quickly. Please wait a moment and try again.",
500: "Internal Server Error",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
};
const errorMessages: { [key: number]: string } = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
408: 'Request Timeout',
429: "You're making requests too quickly. Please wait a moment and try again.",
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
}
const message =
statusCode && statusCode in errorMessages
? errorMessages[statusCode]
: `HTTP Error: ${statusCode || "unknown"} ${statusText}`;
const message =
statusCode && statusCode in errorMessages
? errorMessages[statusCode]
: `HTTP Error: ${statusCode || 'unknown'} ${statusText}`
const isRetryable = statusCode ? [408, 429].includes(statusCode) : false;
const is5xxRetryable =
statusCode && statusCode >= 500 && statusCode < 600 && method === "GET" && attempts === 1;
const isRetryable = statusCode ? [408, 429].includes(statusCode) : false
const is5xxRetryable =
statusCode && statusCode >= 500 && statusCode < 600 && method === 'GET' && attempts === 1
if (!(isRetryable || is5xxRetryable) || attempts >= maxAttempts) {
console.error("Fetch error:", error);
if (!(isRetryable || is5xxRetryable) || attempts >= maxAttempts) {
console.error('Fetch error:', error)
const fetchError = new ModrinthServersFetchError(
`[Modrinth Servers] ${error.message}`,
statusCode,
error,
);
throw new ModrinthServerError(
`[Modrinth Servers] ${message}`,
statusCode,
fetchError,
module,
v1Error,
);
}
const fetchError = new ModrinthServersFetchError(
`[Modrinth Servers] ${error.message}`,
statusCode,
error,
)
throw new ModrinthServerError(
`[Modrinth Servers] ${message}`,
statusCode,
fetchError,
module,
v1Error,
)
}
const baseDelay = statusCode && statusCode >= 500 ? 5000 : 1000;
const delay = Math.min(baseDelay * Math.pow(2, attempts - 1) + Math.random() * 1000, 15000);
console.warn(`Retrying request in ${delay}ms (attempt ${attempts}/${maxAttempts - 1})`);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
const baseDelay = statusCode && statusCode >= 500 ? 5000 : 1000
const delay = Math.min(baseDelay * Math.pow(2, attempts - 1) + Math.random() * 1000, 15000)
console.warn(`Retrying request in ${delay}ms (attempt ${attempts}/${maxAttempts - 1})`)
await new Promise((resolve) => setTimeout(resolve, delay))
continue
}
console.error("Unexpected fetch error:", error);
const fetchError = new ModrinthServersFetchError(
"[Modrinth Servers] An unexpected error occurred during the fetch operation.",
undefined,
error as Error,
);
throw new ModrinthServerError(
"Unexpected error during fetch operation",
undefined,
fetchError,
module,
);
}
}
console.error('Unexpected fetch error:', error)
const fetchError = new ModrinthServersFetchError(
'[Modrinth Servers] An unexpected error occurred during the fetch operation.',
undefined,
error as Error,
)
throw new ModrinthServerError(
'Unexpected error during fetch operation',
undefined,
fetchError,
module,
)
}
}
console.error("All retry attempts failed:", lastError);
if (lastError instanceof FetchError) {
const statusCode = lastError.response?.status;
const pyroError = new ModrinthServersFetchError(
"Maximum retry attempts reached",
statusCode,
lastError,
);
throw new ModrinthServerError("Maximum retry attempts reached", statusCode, pyroError, module);
}
console.error('All retry attempts failed:', lastError)
if (lastError instanceof FetchError) {
const statusCode = lastError.response?.status
const pyroError = new ModrinthServersFetchError(
'Maximum retry attempts reached',
statusCode,
lastError,
)
throw new ModrinthServerError('Maximum retry attempts reached', statusCode, pyroError, module)
}
const fetchError = new ModrinthServersFetchError(
"Maximum retry attempts reached",
undefined,
lastError || undefined,
);
throw new ModrinthServerError("Maximum retry attempts reached", undefined, fetchError, module);
const fetchError = new ModrinthServersFetchError(
'Maximum retry attempts reached',
undefined,
lastError || undefined,
)
throw new ModrinthServerError('Maximum retry attempts reached', undefined, fetchError, module)
}

View File

@@ -1,64 +1,64 @@
import tags from "~/generated/state.json";
import tags from '~/generated/state.json'
export const useTags = () =>
useState("tags", () => ({
categories: tags.categories,
loaders: tags.loaders,
gameVersions: tags.gameVersions,
donationPlatforms: tags.donationPlatforms,
reportTypes: tags.reportTypes,
projectTypes: [
{
actual: "mod",
id: "mod",
display: "mod",
},
{
actual: "mod",
id: "plugin",
display: "plugin",
},
{
actual: "mod",
id: "datapack",
display: "data pack",
},
{
actual: "shader",
id: "shader",
display: "shader",
},
{
actual: "resourcepack",
id: "resourcepack",
display: "resource pack",
},
{
actual: "modpack",
id: "modpack",
display: "modpack",
},
],
loaderData: {
pluginLoaders: ["bukkit", "spigot", "paper", "purpur", "sponge", "folia"],
pluginPlatformLoaders: ["bungeecord", "waterfall", "velocity"],
allPluginLoaders: [
"bukkit",
"spigot",
"paper",
"purpur",
"sponge",
"bungeecord",
"waterfall",
"velocity",
"folia",
],
dataPackLoaders: ["datapack"],
modLoaders: ["forge", "fabric", "quilt", "liteloader", "modloader", "rift", "neoforge"],
hiddenModLoaders: ["liteloader", "modloader", "rift"],
},
projectViewModes: ["list", "grid", "gallery"],
approvedStatuses: ["approved", "archived", "unlisted", "private"],
rejectedStatuses: ["rejected", "withheld"],
staffRoles: ["moderator", "admin"],
}));
useState('tags', () => ({
categories: tags.categories,
loaders: tags.loaders,
gameVersions: tags.gameVersions,
donationPlatforms: tags.donationPlatforms,
reportTypes: tags.reportTypes,
projectTypes: [
{
actual: 'mod',
id: 'mod',
display: 'mod',
},
{
actual: 'mod',
id: 'plugin',
display: 'plugin',
},
{
actual: 'mod',
id: 'datapack',
display: 'data pack',
},
{
actual: 'shader',
id: 'shader',
display: 'shader',
},
{
actual: 'resourcepack',
id: 'resourcepack',
display: 'resource pack',
},
{
actual: 'modpack',
id: 'modpack',
display: 'modpack',
},
],
loaderData: {
pluginLoaders: ['bukkit', 'spigot', 'paper', 'purpur', 'sponge', 'folia'],
pluginPlatformLoaders: ['bungeecord', 'waterfall', 'velocity'],
allPluginLoaders: [
'bukkit',
'spigot',
'paper',
'purpur',
'sponge',
'bungeecord',
'waterfall',
'velocity',
'folia',
],
dataPackLoaders: ['datapack'],
modLoaders: ['forge', 'fabric', 'quilt', 'liteloader', 'modloader', 'rift', 'neoforge'],
hiddenModLoaders: ['liteloader', 'modloader', 'rift'],
},
projectViewModes: ['list', 'grid', 'gallery'],
approvedStatuses: ['approved', 'archived', 'unlisted', 'private'],
rejectedStatuses: ['rejected', 'withheld'],
staffRoles: ['moderator', 'admin'],
}))

View File

@@ -1,38 +1,38 @@
import { injectNotificationManager } from "@modrinth/ui";
import { injectNotificationManager } from '@modrinth/ui'
type AsyncFunction<TArgs extends any[], TResult> = (...args: TArgs) => Promise<TResult>;
type ErrorFunction = (err: any) => void | Promise<void>;
type VoidFunction = () => void | Promise<void>;
type AsyncFunction<TArgs extends any[], TResult> = (...args: TArgs) => Promise<TResult>
type ErrorFunction = (err: any) => void | Promise<void>
type VoidFunction = () => void | Promise<void>
type useClientTry = <TArgs extends any[], TResult>(
fn: AsyncFunction<TArgs, TResult>,
onFail?: ErrorFunction,
onFinish?: VoidFunction,
) => (...args: TArgs) => Promise<TResult | undefined>;
fn: AsyncFunction<TArgs, TResult>,
onFail?: ErrorFunction,
onFinish?: VoidFunction,
) => (...args: TArgs) => Promise<TResult | undefined>
const defaultOnError: ErrorFunction = (error) => {
const { addNotification } = injectNotificationManager();
addNotification({
title: "An error occurred",
text: error?.data?.description || error.message || error || "Unknown error",
type: "error",
});
};
const { addNotification } = injectNotificationManager()
addNotification({
title: 'An error occurred',
text: error?.data?.description || error.message || error || 'Unknown error',
type: 'error',
})
}
export const useClientTry: useClientTry =
(fn, onFail = defaultOnError, onFinish) =>
async (...args) => {
startLoading();
try {
return await fn(...args);
} catch (err) {
if (onFail) {
await onFail(err);
} else {
console.error("[CLIENT TRY ERROR]", err);
}
} finally {
if (onFinish) await onFinish();
stopLoading();
}
};
(fn, onFail = defaultOnError, onFinish) =>
async (...args) => {
startLoading()
try {
return await fn(...args)
} catch (err) {
if (onFail) {
await onFail(err)
} else {
console.error('[CLIENT TRY ERROR]', err)
}
} finally {
if (onFinish) await onFinish()
stopLoading()
}
}

View File

@@ -1,177 +1,179 @@
export const useUser = async (force = false) => {
const user = useState("user", () => {});
const user = useState('user', () => {})
if (!user.value || force || (user.value && Date.now() - user.value.lastUpdated > 300000)) {
user.value = await initUser();
}
if (!user.value || force || (user.value && Date.now() - user.value.lastUpdated > 300000)) {
user.value = await initUser()
}
return user;
};
return user
}
export const initUser = async () => {
const auth = (await useAuth()).value;
const auth = (await useAuth()).value
const user = {
collections: [],
follows: [],
subscriptions: [],
lastUpdated: 0,
};
const user = {
collections: [],
follows: [],
subscriptions: [],
lastUpdated: 0,
}
if (auth.user && auth.user.id) {
try {
const headers = {
Authorization: auth.token,
};
if (auth.user && auth.user.id) {
try {
const headers = {
Authorization: auth.token,
}
const [follows, collections, subscriptions] = await Promise.all([
useBaseFetch(`user/${auth.user.id}/follows`, { headers }, true),
useBaseFetch(`user/${auth.user.id}/collections`, { apiVersion: 3, headers }, true),
useBaseFetch(`billing/subscriptions`, { internal: true, headers }, true),
]);
const [follows, collections, subscriptions] = await Promise.all([
useBaseFetch(`user/${auth.user.id}/follows`, { headers }, true),
useBaseFetch(`user/${auth.user.id}/collections`, { apiVersion: 3, headers }, true),
useBaseFetch(`billing/subscriptions`, { internal: true, headers }, true),
])
user.collections = collections;
user.follows = follows;
user.subscriptions = subscriptions;
user.lastUpdated = Date.now();
} catch (err) {
console.error(err);
}
}
user.collections = collections
user.follows = follows
user.subscriptions = subscriptions
user.lastUpdated = Date.now()
} catch (err) {
console.error(err)
}
}
return user;
};
return user
}
export const initUserCollections = async () => {
const auth = (await useAuth()).value;
const user = (await useUser()).value;
const auth = (await useAuth()).value
const user = (await useUser()).value
if (auth.user && auth.user.id) {
try {
user.collections = await useBaseFetch(`user/${auth.user.id}/collections`, { apiVersion: 3 });
} catch (err) {
console.error(err);
}
}
};
if (auth.user && auth.user.id) {
try {
user.collections = await useBaseFetch(`user/${auth.user.id}/collections`, {
apiVersion: 3,
})
} catch (err) {
console.error(err)
}
}
}
export const initUserFollows = async () => {
const auth = (await useAuth()).value;
const user = (await useUser()).value;
const auth = (await useAuth()).value
const user = (await useUser()).value
if (auth.user && auth.user.id) {
try {
user.follows = await useBaseFetch(`user/${auth.user.id}/follows`);
} catch (err) {
console.error(err);
}
}
};
if (auth.user && auth.user.id) {
try {
user.follows = await useBaseFetch(`user/${auth.user.id}/follows`)
} catch (err) {
console.error(err)
}
}
}
export const initUserProjects = async () => {
const auth = (await useAuth()).value;
const user = (await useUser()).value;
const auth = (await useAuth()).value
const user = (await useUser()).value
if (auth.user && auth.user.id) {
try {
user.projects = await useBaseFetch(`user/${auth.user.id}/projects`);
} catch (err) {
console.error(err);
}
}
};
if (auth.user && auth.user.id) {
try {
user.projects = await useBaseFetch(`user/${auth.user.id}/projects`)
} catch (err) {
console.error(err)
}
}
}
export const userCollectProject = async (collection, projectId) => {
const user = (await useUser()).value;
await initUserCollections();
const user = (await useUser()).value
await initUserCollections()
const collectionId = collection.id;
const collectionId = collection.id
const latestCollection = user.collections.find((x) => x.id === collectionId);
if (!latestCollection) {
throw new Error("This collection was not found. Has it been deleted?");
}
const latestCollection = user.collections.find((x) => x.id === collectionId)
if (!latestCollection) {
throw new Error('This collection was not found. Has it been deleted?')
}
const add = !latestCollection.projects.includes(projectId);
const projects = add
? [...latestCollection.projects, projectId]
: [...latestCollection.projects].filter((x) => x !== projectId);
const add = !latestCollection.projects.includes(projectId)
const projects = add
? [...latestCollection.projects, projectId]
: [...latestCollection.projects].filter((x) => x !== projectId)
const idx = user.collections.findIndex((x) => x.id === latestCollection.id);
if (idx >= 0) {
user.collections[idx].projects = projects;
}
const idx = user.collections.findIndex((x) => x.id === latestCollection.id)
if (idx >= 0) {
user.collections[idx].projects = projects
}
await useBaseFetch(`collection/${collection.id}`, {
method: "PATCH",
body: {
new_projects: projects,
},
apiVersion: 3,
});
};
await useBaseFetch(`collection/${collection.id}`, {
method: 'PATCH',
body: {
new_projects: projects,
},
apiVersion: 3,
})
}
export const userFollowProject = async (project) => {
const user = (await useUser()).value;
const user = (await useUser()).value
if (user.follows.find((x) => x.id === project.id)) {
user.follows = user.follows.filter((x) => x.id !== project.id);
project.followers--;
if (user.follows.find((x) => x.id === project.id)) {
user.follows = user.follows.filter((x) => x.id !== project.id)
project.followers--
setTimeout(() => {
useBaseFetch(`project/${project.id}/follow`, {
method: "DELETE",
});
});
} else {
user.follows = user.follows.concat(project);
project.followers++;
setTimeout(() => {
useBaseFetch(`project/${project.id}/follow`, {
method: 'DELETE',
})
})
} else {
user.follows = user.follows.concat(project)
project.followers++
setTimeout(() => {
useBaseFetch(`project/${project.id}/follow`, {
method: "POST",
});
});
}
};
setTimeout(() => {
useBaseFetch(`project/${project.id}/follow`, {
method: 'POST',
})
})
}
}
export const resendVerifyEmail = async () => {
// const { injectNotificationManager } = await import("@modrinth/ui");
// const { addNotification } = injectNotificationManager();
// const { injectNotificationManager } = await import("@modrinth/ui");
// const { addNotification } = injectNotificationManager();
startLoading();
try {
await useBaseFetch("auth/email/resend_verify", {
method: "POST",
});
startLoading()
try {
await useBaseFetch('auth/email/resend_verify', {
method: 'POST',
})
const auth = await useAuth();
addNotification({
title: "Email sent",
text: `An email with a link to verify your account has been sent to ${auth.value.user.email}.`,
type: "success",
});
} catch (err) {
addNotification({
title: "An error occurred",
text: err.data.description,
type: "error",
});
}
stopLoading();
};
const auth = await useAuth()
addNotification({
title: 'Email sent',
text: `An email with a link to verify your account has been sent to ${auth.value.user.email}.`,
type: 'success',
})
} catch (err) {
addNotification({
title: 'An error occurred',
text: err.data.description,
type: 'error',
})
}
stopLoading()
}
export const logout = async () => {
startLoading();
const auth = await useAuth();
try {
await useBaseFetch(`session/${auth.value.token}`, {
method: "DELETE",
});
} catch {
/* empty */
}
startLoading()
const auth = await useAuth()
try {
await useBaseFetch(`session/${auth.value.token}`, {
method: 'DELETE',
})
} catch {
/* empty */
}
await useAuth("none");
useCookie("auth-token").value = null;
stopLoading();
};
await useAuth('none')
useCookie('auth-token').value = null
stopLoading()
}

View File

@@ -1,12 +1,12 @@
export const useNotificationRightwards = () => {
const isVisible = useState("moderation-checklist-notifications", () => false);
const isVisible = useState('moderation-checklist-notifications', () => false)
const setVisible = (visible: boolean) => {
isVisible.value = visible;
};
const setVisible = (visible: boolean) => {
isVisible.value = visible
}
return {
isVisible: readonly(isVisible),
setVisible,
};
};
return {
isVisible: readonly(isVisible),
setVisible,
}
}

View File

@@ -4,11 +4,11 @@
* @returns A computed reference that changes when component becomes mounted or unmounted.
*/
export function useMountedValue<T>(getter: (isMounted: boolean) => T) {
const mounted = ref(getCurrentInstance()?.isMounted ?? false);
const mounted = ref(getCurrentInstance()?.isMounted ?? false)
onMounted(() => (mounted.value = true));
onMounted(() => (mounted.value = true))
onUnmounted(() => (mounted.value = false));
onUnmounted(() => (mounted.value = false))
return computed(() => getter(mounted.value));
return computed(() => getter(mounted.value))
}