diff --git a/package-lock.json b/package-lock.json index c9dc312..fd0b735 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2778,6 +2778,22 @@ "mime-types": "^2.1.12" } }, + "formdata-node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.2.2.tgz", + "integrity": "sha512-rDQeb6tk/Noep0MXvKhctr5x3gSpeJ+e5gYLFGM4jAt0MilYQLDR1jq61u60Piig3/EtxB/xBZ8WLTmunE5BoA==", + "requires": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.1" + }, + "dependencies": { + "web-streams-polyfill": { + "version": "4.0.0-beta.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.1.tgz", + "integrity": "sha512-3ux37gEX670UUphBF9AMCq8XM6iQ8Ac6A+DSRRjDoRBm1ufCkaCDdNVbaqq60PsEkdNlLKrGtv/YBP4EJXqNtQ==" + } + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3923,6 +3939,11 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + }, "node-fetch": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0.tgz", diff --git a/package.json b/package.json index d145c7c..4993f89 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "dependencies": { "@actions/core": "^1.5.0", "fast-glob": "^3.2.7", + "formdata-node": "^4.2.2", "node-fetch": "^3.0.0" } } diff --git a/src/utils/curseforge-utils.ts b/src/utils/curseforge-utils.ts new file mode 100644 index 0000000..019f2cf --- /dev/null +++ b/src/utils/curseforge-utils.ts @@ -0,0 +1,104 @@ +import fetch from "node-fetch"; +import { FormData } from "formdata-node"; +import { fileFromPath } from "formdata-node/file-from-path"; +import { File } from "../utils/file-utils"; +import { findVersionByName } from "./minecraft-utils"; + +const baseUrl = "https://minecraft.curseforge.com/api"; + +interface CurseForgeVersion { + id: number; + gameVersionTypeID: number; + name: string; + slug: string; +} + +interface CurseForgeVersions { + gameVersions: CurseForgeVersion[]; + loaders: CurseForgeVersion[]; + java: CurseForgeVersion[]; +} + + +let cachedCurseForgeVersions: CurseForgeVersions = null; +async function getCurseForgeVersions(token: string): Promise { + if (!cachedCurseForgeVersions) { + cachedCurseForgeVersions = await loadCurseForgeVersions(token); + } + return cachedCurseForgeVersions; +} + +async function loadCurseForgeVersions(token: string): Promise { + const versionTypes = <{ id: number, slug: string }[]>await (await fetch(`${baseUrl}/game/version-types?token=${token}`)).json(); + const javaVersionTypes = versionTypes.filter(x => x.slug.startsWith("java")).map(x => x.id); + const minecraftVersionTypes = versionTypes.filter(x => x.slug.startsWith("minecraft")).map(x => x.id); + const loaderVersionTypes = versionTypes.filter(x => x.slug.startsWith("modloader")).map(x => x.id); + + const versions = await (await fetch(`${baseUrl}/game/versions?token=${token}`)).json(); + return versions.reduce((container, version) => { + if (javaVersionTypes.includes(version.gameVersionTypeID)) { + container.java.push(version); + } else if (minecraftVersionTypes.includes(version.gameVersionTypeID)) { + container.gameVersions.push(version); + } else if (loaderVersionTypes.includes(version.gameVersionTypeID)) { + container.loaders.push(version); + } + return container; + }, { gameVersions: new Array(), loaders: new Array(), java: new Array() }); +} + +export async function unifyGameVersion(gameVersion: string): Promise { + gameVersion = gameVersion.trim(); + const minecraftVersion = await findVersionByName(gameVersion); + if (minecraftVersion) { + return `${minecraftVersion.name}${(minecraftVersion.isSnapshot ? "-Snapshot" : "")}`; + } + return gameVersion.replace(/([^\w]|_)+/g, ".").replace(/[.-][a-zA-Z]\w+$/, "-Snapshot"); +} + +export function unifyJava(java: string): string { + java = java.trim(); + const match = java.match(/(?:\d+\D)?(\d+)$/); + if (match && match.length === 2) { + return `Java ${match[1]}`; + } + return java; +} + +async function addVersionIntersectionToSet(curseForgeVersions: CurseForgeVersion[], versions: string[], unify: (v: string) => string | Promise, comparer: (cfv: CurseForgeVersion, v: string) => boolean, intersection: Set ) { + for (const version of versions) { + const unifiedVersion = await unify(version); + const curseForgeVersion = curseForgeVersions.find(x => comparer(x, unifiedVersion)); + if (curseForgeVersion) { + intersection.add(curseForgeVersion.id); + } + } +} + +export async function convertToCurseForgeVersions(gameVersions: string[], loaders: string[], java: string[], token: string): Promise { + const versions = new Set(); + const curseForgeVersions = await getCurseForgeVersions(token); + + await addVersionIntersectionToSet(curseForgeVersions.gameVersions, gameVersions, unifyGameVersion, (cfv, v) => cfv.name === v, versions); + await addVersionIntersectionToSet(curseForgeVersions.loaders, loaders, x => x.trim().toLowerCase(), (cfv, v) => cfv.slug === v, versions); + await addVersionIntersectionToSet(curseForgeVersions.java, java, unifyJava, (cfv, v) => cfv.name === v, versions); + + return [...versions]; +} + +export async function uploadFile(id: string, data: Record, file: File, token: string): Promise { + const form = new FormData(); + form.append("file", await fileFromPath(file.path), file.name); + form.append("metadata", JSON.stringify(data)); + + const response = await fetch(`${baseUrl}/projects/${id}/upload-file?token=${token}`, { + method: "POST", + body: form + }); + + if (!response.ok) { + throw new Error(`Failed to upload file: ${response.status} (${response.statusText})`) + } + + return (<{ id: number }>await response.json()).id; +}