You've already forked AstralRinth
forked from didirus/AstralRinth
099011a177
* feat: api-client module for content v0 * feat: delete unused components + modules + setting * feat: xhr uploading * feat: fs module -> api-client * feat: migrate files.vue to use tanstack * fix: mem leak + other issues * fix: build * feat: switch to monaco * fix: go back to using ace, but improve preloading + theme * fix: styling + dead attrs * feat: match figma * fix: padding * feat: files-new for ui page structure * feat: finalize files.vue * fix: lint * fix: qa * fix: dep * fix: lint * fix: lockfile merge * feat: icons on navtab * fix: surface alternating on table * fix: hover surface color --------- Signed-off-by: Calum H. <contact@cal.engineer> Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
288 lines
8.0 KiB
TypeScript
288 lines
8.0 KiB
TypeScript
import type { AbstractWebNotificationManager } from '@modrinth/ui'
|
|
import type { JWTAuth, ModuleError, ModuleName } from '@modrinth/utils'
|
|
import { ModrinthServerError } from '@modrinth/utils'
|
|
|
|
import { ContentModule, GeneralModule, NetworkModule, StartupModule } from './modules/index.ts'
|
|
import { useServersFetch } from './servers-fetch.ts'
|
|
|
|
export function handleServersError(err: any, notifications: AbstractWebNotificationManager) {
|
|
if (err instanceof ModrinthServerError && err.v1Error) {
|
|
notifications.addNotification({
|
|
title: err.v1Error?.context ?? `An error occurred`,
|
|
type: 'error',
|
|
text: err.v1Error.description,
|
|
errorCode: err.v1Error.error,
|
|
})
|
|
} else {
|
|
notifications.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 general: GeneralModule
|
|
readonly content: ContentModule
|
|
readonly network: NetworkModule
|
|
readonly startup: StartupModule
|
|
|
|
constructor(serverId: string) {
|
|
this.serverId = serverId
|
|
|
|
this.general = new GeneralModule(this)
|
|
this.content = new ContentModule(this)
|
|
this.network = new NetworkModule(this)
|
|
this.startup = new StartupModule(this)
|
|
}
|
|
|
|
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`
|
|
|
|
for (const [key, value] of Object.entries(properties)) {
|
|
if (typeof value === 'object') {
|
|
fileContent += `${key}=${JSON.stringify(value)}\n`
|
|
} else if (typeof value === 'boolean') {
|
|
fileContent += `${key}=${value ? 'true' : 'false'}\n`
|
|
} else {
|
|
fileContent += `${key}=${value}\n`
|
|
}
|
|
}
|
|
|
|
return fileContent
|
|
}
|
|
|
|
async processImage(iconUrl: string | undefined): Promise<string | undefined> {
|
|
const sharedImage = useState<string | undefined>(`server-icon-${this.serverId}`)
|
|
|
|
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
|
|
})
|
|
|
|
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 (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
|
|
}
|
|
|
|
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`
|
|
|
|
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.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
|
|
}
|
|
}
|
|
|
|
async refresh(
|
|
modules: ModuleName[] = [],
|
|
options?: {
|
|
preserveConnection?: boolean
|
|
preserveInstallState?: boolean
|
|
},
|
|
): Promise<void> {
|
|
const modulesToRefresh =
|
|
modules.length > 0 ? modules : (['general', 'content', 'network', 'startup'] as ModuleName[])
|
|
|
|
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
|
|
|
|
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 'network':
|
|
await this.network.fetch()
|
|
break
|
|
case 'startup':
|
|
await this.startup.fetch()
|
|
break
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof ModrinthServerError) {
|
|
if (error.statusCode === 404 && module === 'content') {
|
|
console.debug(`Optional ${module} resource not found:`, 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(),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
get moduleErrors() {
|
|
return this.errors
|
|
}
|
|
}
|
|
|
|
export const useModrinthServers = async (
|
|
serverId: string,
|
|
includedModules: ModuleName[] = ['general'],
|
|
) => {
|
|
const server = new ModrinthServer(serverId)
|
|
await server.refresh(includedModules)
|
|
return reactive(server)
|
|
}
|