diff --git a/src/platforms/curseforge/index.ts b/src/platforms/curseforge/index.ts new file mode 100644 index 0000000..94f79dd --- /dev/null +++ b/src/platforms/curseforge/index.ts @@ -0,0 +1,66 @@ +export { + CurseForgeUploadApiClient, + CurseForgeUploadApiOptions, + + CURSEFORGE_UPLOAD_API_URL, +} from "./curseforge-upload-api-client"; + +export { + CurseForgeEternalApiClient, + CurseForgeEternalApiOptions, + + CURSEFORGE_ETERNAL_API_URL, +} from "./curseforge-eternal-api-client"; + +export { + CurseForgeDependency, +} from "./curseforge-dependency"; + +export { + CurseForgeDependencyType, +} from "./curseforge-dependency-type"; + +export { + CurseForgeGameVersion, + + CURSEFORGE_GAME_VERSION_PLUGIN_NAME_COMPARER, + CURSEFORGE_GAME_VERSION_SNAPSHOT_NAME_COMPARER, + + findCurseForgeGameVersionIdsByNames, +} from "./curseforge-game-version"; + +export { + CurseForgeGameVersionMap, + + createCurseForgeGameVersionMap, +} from "./curseforge-game-version-map"; + +export { + CurseForgeGameVersionType, +} from "./curseforge-game-version-type"; + +export { + CurseForgeGameVersionUnion, +} from "./curseforge-game-version-union"; + +export { + CurseForgeProject, + + isCurseForgeProjectId, +} from "./curseforge-project"; + +export { + CurseForgeUploader, + CurseForgeUploaderOptions, + CurseForgeUploadRequest, + CurseForgeUploadReport, +} from "./curseforge-uploader"; + +export { + CurseForgeVersion, + CurseForgeVersionInit, +} from "./curseforge-version"; + +export { + CurseForgeFile, +} from "./curseforge-file"; diff --git a/src/utils/curseforge/index.ts b/src/utils/curseforge/index.ts deleted file mode 100644 index 60025ff..0000000 --- a/src/utils/curseforge/index.ts +++ /dev/null @@ -1,153 +0,0 @@ -import fetch from "node-fetch"; -import FormData from "form-data"; -import File from "../io/file"; -import { findVersionByName } from "../minecraft"; -import SoftError from "../soft-error"; - -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[]; -} - -interface CurseForgeUploadErrorInfo { - errorCode: number; - errorMessage: string; -} - -class CurseForgeUploadError extends SoftError { - public readonly info?: CurseForgeUploadErrorInfo; - - constructor(soft: boolean, message?: string, info?: CurseForgeUploadErrorInfo) { - super(soft, message); - this.info = info; - } -} - -async function fetchJsonArray(url: string): Promise { - const response = await fetch(url); - if (!response.ok) { - const isSoft = response.status === 429 || response.status >= 500; - throw new SoftError(isSoft, `${response.status} (${response.statusText})`); - } - - let array: T[]; - try { - array = await response.json(); - } catch { - array = null; - } - - if (!Array.isArray(array)) { - throw new SoftError(true, "CurseForge sometimes returns Cloudflare's HTML page instead of its API response. Yeah, I know, very cool. Just wait 15-20 minutes, then try re-running this action, and you should be fine."); - } - return array; -} - -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 = await fetchJsonArray<{ id: number, slug: string }>(`${baseUrl}/game/version-types?token=${token}`); - 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 fetchJsonArray(`${baseUrl}/game/versions?token=${token}`); - 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 { - if (Array.isArray(data.relations?.projects) && (!data.relations.projects.length || data.parentFileID)) { - delete data.relations; - } - - if (data.gameVersions && data.parentFileID) { - delete data.gameVersions; - } - - const form = new FormData(); - form.append("file", file.getStream(), file.name); - form.append("metadata", JSON.stringify(data)); - - const response = await fetch(`${baseUrl}/projects/${id}/upload-file?token=${token}`, { - method: "POST", - headers: form.getHeaders(), - body: form - }); - - if (!response.ok) { - let errorText = response.statusText; - let info: CurseForgeUploadErrorInfo; - try { - info = await response.json(); - errorText += `, ${JSON.stringify(info)}`; - } catch { } - const isSoftError = response.status === 429 || response.status >= 500; - throw new CurseForgeUploadError(isSoftError, `Failed to upload file: ${response.status} (${errorText})`, info); - } - - return (<{ id: number }>await response.json()).id; -}