mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2024-11-22 00:11:02 -05:00
Refactored CurseForgePublisher
(-> CurseForgeUploader
)
This commit is contained in:
parent
0203767159
commit
9d0bdf20b7
2 changed files with 145 additions and 70 deletions
145
src/platforms/curseforge/curseforge-uploader.ts
Normal file
145
src/platforms/curseforge/curseforge-uploader.ts
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
import { CurseForgeUploadRequest as UploadRequest, CurseForgeUploadReport as UploadReport } from "@/action";
|
||||||
|
import { Dependency } from "@/dependencies";
|
||||||
|
import { PlatformType } from "@/platforms/platform-type";
|
||||||
|
import { GenericPlatformUploader, GenericPlatformUploaderOptions } from "@/platforms/generic-platform-uploader";
|
||||||
|
import { CurseForgeDependency } from "./curseforge-dependency";
|
||||||
|
import { CurseForgeDependencyType } from "./curseforge-dependency-type";
|
||||||
|
import { CurseForgeEternalApiClient } from "./curseforge-eternal-api-client";
|
||||||
|
import { CurseForgeProject, isCurseForgeProjectId } from "./curseforge-project";
|
||||||
|
import { CurseForgeUploadApiClient } from "./curseforge-upload-api-client";
|
||||||
|
import { CurseForgeVersion } from "./curseforge-version";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration options for the uploader, tailored for use with CurseForge.
|
||||||
|
*/
|
||||||
|
export type CurseForgeUploaderOptions = GenericPlatformUploaderOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the structure for an upload request, adapted for use with CurseForge.
|
||||||
|
*/
|
||||||
|
export type CurseForgeUploadRequest = UploadRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies the structure of the report generated after a successful upload to CurseForge.
|
||||||
|
*/
|
||||||
|
export type CurseForgeUploadReport = UploadReport;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the uploader for CurseForge.
|
||||||
|
*/
|
||||||
|
export class CurseForgeUploader extends GenericPlatformUploader<CurseForgeUploaderOptions, CurseForgeUploadRequest, CurseForgeUploadReport> {
|
||||||
|
/**
|
||||||
|
* Constructs a new {@link CurseForgeUploader} instance.
|
||||||
|
*
|
||||||
|
* @param options - The options to use for the uploader.
|
||||||
|
*/
|
||||||
|
constructor(options?: CurseForgeUploaderOptions) {
|
||||||
|
super(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
get platform(): PlatformType {
|
||||||
|
return PlatformType.CURSEFORGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async uploadCore(request: CurseForgeUploadRequest): Promise<CurseForgeUploadReport> {
|
||||||
|
const api = new CurseForgeUploadApiClient({ token: request.token.unwrap() });
|
||||||
|
const eternalApi = new CurseForgeEternalApiClient();
|
||||||
|
|
||||||
|
const project = await this.getProject(request.id, eternalApi);
|
||||||
|
const version = await this.createVersion(request, project.id, api, eternalApi);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: project.id,
|
||||||
|
version: version.id,
|
||||||
|
url: `${project.links.websiteUrl}/files/${version.id}`,
|
||||||
|
files: version.files.map(x => ({ id: x.id, name: x.name, url: x.url })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the project details from CurseForge.
|
||||||
|
*
|
||||||
|
* @param idOrSlug - The identifier or slug of the project.
|
||||||
|
* @param eternalApi - The API client instance to use for the request.
|
||||||
|
*
|
||||||
|
* @returns A promise resolved with the fetched project details.
|
||||||
|
*/
|
||||||
|
private async getProject(idOrSlug: number | string, eternalApi: CurseForgeEternalApiClient): Promise<CurseForgeProject> {
|
||||||
|
const project = await eternalApi.getProject(idOrSlug).catch(() => undefined as CurseForgeProject);
|
||||||
|
if (project) {
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCurseForgeProjectId(idOrSlug)) {
|
||||||
|
throw new Error(`Cannot access CurseForge project "${idOrSlug}" by its slug. Please specify the ID instead.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the project was not found, it could imply two situations:
|
||||||
|
// 1) The project is not publicly visible.
|
||||||
|
// 2) CurseForge is notorious for its frequent downtime. There's a significant probability that
|
||||||
|
// we attempted to access their API during one of those periods.
|
||||||
|
//
|
||||||
|
// Regardless, if the user provided us with a project ID, that's all we need
|
||||||
|
// to attempt publishing their assets. Although the upload report may be imprecise
|
||||||
|
// with this placeholder data, it's still preferable to not uploading anything at all.
|
||||||
|
this._logger.debug(`CurseForge project "${idOrSlug}" is inaccessible.`);
|
||||||
|
return {
|
||||||
|
id: +idOrSlug,
|
||||||
|
slug: String(idOrSlug),
|
||||||
|
links: { websiteUrl: `https://www.curseforge.com/minecraft/mc-mods/${idOrSlug}` }
|
||||||
|
} as CurseForgeProject;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new version of the project on CurseForge.
|
||||||
|
*
|
||||||
|
* @param request - The upload request containing information about the new version.
|
||||||
|
* @param projectId - The identifier of the project.
|
||||||
|
* @param api - The API client instance to use for the upload request.
|
||||||
|
* @param eternalApi - The API client instance to use for retrieving data.
|
||||||
|
*
|
||||||
|
* @returns The details of the newly created version.
|
||||||
|
*/
|
||||||
|
private async createVersion(request: CurseForgeUploadRequest, projectId: number, api: CurseForgeUploadApiClient, eternalApi: CurseForgeEternalApiClient): Promise<CurseForgeVersion> {
|
||||||
|
const dependencies = await this.convertToCurseForgeDependencies(request.dependencies, eternalApi);
|
||||||
|
|
||||||
|
return await api.createVersion({
|
||||||
|
name: request.name,
|
||||||
|
project_id: projectId,
|
||||||
|
version_type: request.versionType,
|
||||||
|
changelog: request.changelog,
|
||||||
|
game_versions: request.gameVersions,
|
||||||
|
java_versions: request.java,
|
||||||
|
loaders: request.loaders,
|
||||||
|
files: request.files,
|
||||||
|
dependencies,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the dependencies to CurseForge-specific format.
|
||||||
|
*
|
||||||
|
* @param dependencies - The list of dependencies to convert.
|
||||||
|
* @param eternalApi - The API client instance to use for retrieving data.
|
||||||
|
*
|
||||||
|
* @returns An array of converted dependencies.
|
||||||
|
*/
|
||||||
|
private async convertToCurseForgeDependencies(dependencies: Dependency[], eternalApi: CurseForgeEternalApiClient): Promise<CurseForgeDependency[]> {
|
||||||
|
const simpleDependencies = this.convertToSimpleDependencies(dependencies, CurseForgeDependencyType.fromDependencyType);
|
||||||
|
const curseforgeDependencies = await Promise.all(simpleDependencies.map(async ([id, type]) => ({
|
||||||
|
slug: isCurseForgeProjectId(id)
|
||||||
|
? await eternalApi.getProject(id).catch(() => undefined as CurseForgeProject).then(x => x?.slug)
|
||||||
|
: id,
|
||||||
|
|
||||||
|
type,
|
||||||
|
})));
|
||||||
|
return curseforgeDependencies.filter(x => x.slug && x.type);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,70 +0,0 @@
|
||||||
import File from "../../utils/io/file";
|
|
||||||
import ModPublisher from "../mod-publisher";
|
|
||||||
import PublisherTarget from "../publisher-target";
|
|
||||||
import { convertToCurseForgeVersions, uploadFile } from "../../utils/curseforge";
|
|
||||||
import Dependency from "../../metadata/dependency";
|
|
||||||
import DependencyKind from "../../metadata/dependency-kind";
|
|
||||||
|
|
||||||
const forgeDependencyKinds = new Map([
|
|
||||||
[DependencyKind.Depends, "requiredDependency"],
|
|
||||||
[DependencyKind.Recommends, "optionalDependency"],
|
|
||||||
[DependencyKind.Suggests, "optionalDependency"],
|
|
||||||
[DependencyKind.Includes, "embeddedLibrary"],
|
|
||||||
[DependencyKind.Breaks, "incompatible"],
|
|
||||||
]);
|
|
||||||
|
|
||||||
export default class CurseForgePublisher extends ModPublisher {
|
|
||||||
public get target(): PublisherTarget {
|
|
||||||
return PublisherTarget.CurseForge;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async publishMod(id: string, token: string, name: string, _version: string, channel: string, loaders: string[], gameVersions: string[], java: string[], changelog: string, files: File[], dependencies: Dependency[]): Promise<void> {
|
|
||||||
let parentFileId = undefined;
|
|
||||||
const versions = await convertToCurseForgeVersions(gameVersions, loaders, java, token);
|
|
||||||
const projects = dependencies
|
|
||||||
.filter((x, _, self) => x.kind !== DependencyKind.Suggests || !self.find(y => y.id === x.id && y.kind !== DependencyKind.Suggests))
|
|
||||||
.map(x => ({
|
|
||||||
slug: x.getProjectSlug(this.target),
|
|
||||||
type: forgeDependencyKinds.get(x.kind)
|
|
||||||
}))
|
|
||||||
.filter(x => x.slug && x.type);
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const data = {
|
|
||||||
changelog,
|
|
||||||
changelogType: "markdown",
|
|
||||||
displayName: (parentFileId || !name) ? file.name : name,
|
|
||||||
parentFileID: parentFileId,
|
|
||||||
releaseType: channel,
|
|
||||||
gameVersions: parentFileId ? undefined : versions,
|
|
||||||
relations: (parentFileId || !projects.length) ? undefined : { projects }
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileId = await this.upload(id, data, file, token);
|
|
||||||
if (!parentFileId) {
|
|
||||||
parentFileId = fileId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async upload(id: string, data: Record<string, any>, file: File, token: string): Promise<number | never> {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
return await uploadFile(id, data, file, token);
|
|
||||||
} catch (error) {
|
|
||||||
if (error?.info?.errorCode === 1018 && typeof error.info.errorMessage === "string") {
|
|
||||||
const match = error.info.errorMessage.match(/Invalid slug in project relations: '([^']+)'/);
|
|
||||||
const projects = <{ slug: string }[]>data.relations?.projects;
|
|
||||||
if (match && projects?.length) {
|
|
||||||
const invalidSlugIndex = projects.findIndex(x => x.slug === match[1]);
|
|
||||||
if (invalidSlugIndex !== -1) {
|
|
||||||
projects.splice(invalidSlugIndex, 1);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue