Refactored ModrinthPublisher (-> ModrinthUploader)

This commit is contained in:
Kir_Antipov 2023-05-01 08:38:11 +00:00
parent a90d0659d7
commit 2c627b694a
2 changed files with 193 additions and 100 deletions

View file

@ -0,0 +1,193 @@
import { ModrinthUploadReport as UploadReport, ModrinthUploadRequest as UploadRequest } from "@/action";
import { Dependency } from "@/dependencies";
import { GenericPlatformUploader, GenericPlatformUploaderOptions } from "@/platforms/generic-platform-uploader";
import { PlatformType } from "@/platforms/platform-type";
import { $i } from "@/utils/collections";
import { IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER } from "@/utils/comparison";
import { ModrinthApiClient } from "./modrinth-api-client";
import { ModrinthDependency } from "./modrinth-dependency";
import { ModrinthDependencyType } from "./modrinth-dependency-type";
import { ModrinthProject } from "./modrinth-project";
import { ModrinthUnfeatureMode } from "./modrinth-unfeature-mode";
import { ModrinthVersion } from "./modrinth-version";
/**
* Configuration options for the uploader, tailored for use with Modrinth.
*/
export type ModrinthUploaderOptions = GenericPlatformUploaderOptions;
/**
* Defines the structure for an upload request, adapted for use with Modrinth.
*/
export type ModrinthUploadRequest = UploadRequest;
/**
* Specifies the structure of the report generated after a successful upload to Modrinth.
*/
export type ModrinthUploadReport = UploadReport;
/**
* Implements the uploader for Modrinth.
*/
export class ModrinthUploader extends GenericPlatformUploader<ModrinthUploaderOptions, ModrinthUploadRequest, ModrinthUploadReport> {
/**
* Constructs a new {@link ModrinthUploader} instance.
*
* @param options - The options to use for the uploader.
*/
constructor(options?: ModrinthUploaderOptions) {
super(options);
}
/**
* @inheritdoc
*/
get platform(): PlatformType {
return PlatformType.MODRINTH;
}
/**
* @inheritdoc
*/
protected async uploadCore(request: ModrinthUploadRequest): Promise<ModrinthUploadReport> {
const api = new ModrinthApiClient({ token: request.token.unwrap() });
const project = await this.getProject(request.id, api);
const version = await this.createVersion(request, project, api);
await this.unfeaturePreviousVersions(version, request.unfeatureMode, api);
return {
id: project.id,
version: version.id,
url: `https://modrinth.com/${project.project_type}/${project.slug}/version/${version.name}`,
files: version.files.map(x => ({ id: x.hashes.sha1, name: x.filename, url: x.url })),
};
}
/**
* Fetches the project details from Modrinth.
*
* @param idOrSlug - The identifier or slug of the project.
* @param api - The API client instance to use for the request.
*
* @returns The fetched project details.
*/
private async getProject(idOrSlug: string, api: ModrinthApiClient): Promise<ModrinthProject> {
const project = await api.getProject(idOrSlug);
if (!project) {
throw new Error(`Modrinth project "${idOrSlug}" was not found.`);
}
return project;
}
/**
* Creates a new version of the project on Modrinth.
*
* @param request - The upload request containing information about the new version.
* @param project - The project for which the new version is created.
* @param api - The API client instance to use for the upload request.
*
* @returns The details of the newly created version.
*/
private async createVersion(request: ModrinthUploadRequest, project: ModrinthProject, api: ModrinthApiClient): Promise<ModrinthVersion> {
const gameVersions = await this.convertToModrinthGameVersionNames(request.gameVersions, api);
const loaders = await this.convertToModrinthLoaderNames(request.loaders, project, api);
const dependencies = await this.convertToModrinthDependencies(request.dependencies, api);
return await api.createVersion({
project_id: project.id,
name: request.name,
version_number: request.version,
changelog: request.changelog,
version_type: request.versionType,
featured: request.featured,
game_versions: gameVersions,
loaders,
dependencies,
});
}
/**
* Converts the dependencies to Modrinth-specific format.
*
* @param dependencies - The list of dependencies to convert.
* @param api - The API client instance to use for retrieving data.
*
* @returns An array of converted dependencies.
*/
private async convertToModrinthDependencies(dependencies: Dependency[], api: ModrinthApiClient): Promise<ModrinthDependency[]> {
const simpleDependencies = this.convertToSimpleDependencies(dependencies, ModrinthDependencyType.fromDependencyType);
const modrinthDependencies = await Promise.all(simpleDependencies.map(async ([id, type]) => ({
project_id: await api.getProjectId(id).catch(() => undefined as string),
dependency_type: type,
})));
return modrinthDependencies.filter(x => x.project_id && x.dependency_type);
}
/**
* Converts loader names to Modrinth-specific format.
*
* @param loaders - The list of loaders to convert.
* @param project - The project for which the loaders are used.
* @param api - The API client instance to use for retrieving data.
*
* @returns An array of converted loader names.
*/
private async convertToModrinthLoaderNames(loaders: string[], project: ModrinthProject, api: ModrinthApiClient): Promise<string[]> {
if (!loaders?.length) {
return [];
}
const modrinthLoaders = await api.getLoaders();
return $i(loaders)
.map(x => modrinthLoaders.find(y => IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER(x, y.name)))
.filter(x => x.supported_project_types?.includes(project.project_type))
.map(x => x.name)
.toArray();
}
/**
* Converts game version names to Modrinth-specific format.
*
* @param gameVersions - The list of game versions to convert.
* @param api - The API client instance to use for retrieving data.
*
* @returns An array of converted game version names.
*/
private async convertToModrinthGameVersionNames(gameVersions: string[], api: ModrinthApiClient): Promise<string[]> {
if (!gameVersions?.length) {
return [];
}
const modrinthGameVersions = await api.getGameVersions();
return $i(gameVersions)
.map(x => modrinthGameVersions.find(y => IGNORE_CASE_AND_NON_WORD_CHARACTERS_EQUALITY_COMPARER(x, y.version))?.version)
.filter(x => x)
.toArray();
}
/**
* Unfeatures previous versions of the project on Modrinth.
*
* @param version - The new version after which the previous ones should be unfeatured.
* @param unfeatureMode - The mode to determine which versions should be unfeatured.
* @param api - The API client instance to use for the unfeaturing request.
*/
private async unfeaturePreviousVersions(version: ModrinthVersion, unfeatureMode: ModrinthUnfeatureMode, api: ModrinthApiClient): Promise<void> {
if (unfeatureMode === ModrinthUnfeatureMode.NONE) {
return;
}
this._logger.info(`🔽 Initiating unfeaturing of older Modrinth project versions`);
const result = await api.unfeaturePreviousProjectVersions(version, unfeatureMode);
const unfeaturedVersions = Object.entries(result).filter(([, success]) => success).map(([version]) => version);
const nonUnfeaturedVersions = Object.entries(result).filter(([, success]) => !success).map(([version]) => version);
if (unfeaturedVersions.length) {
this._logger.info(`🟢 Successfully unfeatured ${unfeaturedVersions.join(", ")}`);
}
if (nonUnfeaturedVersions.length) {
this._logger.info(`⚠️ Failed to unfeature ${nonUnfeaturedVersions.join(", ")}. Please, double-check your token`);
}
}
}

View file

@ -1,100 +0,0 @@
import { createVersion, getProject, getVersions, modifyVersion } from "../../utils/modrinth";
import File from "../../utils/io/file";
import ModPublisher from "../mod-publisher";
import PublisherTarget from "../publisher-target";
import Dependency from "../../metadata/dependency";
import DependencyKind from "../../metadata/dependency-kind";
import { mapBooleanInput, mapEnumInput } from "../../utils/actions/input";
import LoggingStopwatch from "../../utils/logging/logging-stopwatch";
enum UnfeatureMode {
None = 0,
VersionSubset = 1,
VersionIntersection = 2,
VersionAny = 4,
LoaderSubset = 8,
LoaderIntersection = 16,
LoaderAny = 32,
Subset = VersionSubset | LoaderSubset,
Intersection = VersionIntersection | LoaderIntersection,
Any = VersionAny | LoaderAny,
}
function hasFlag(unfeatureMode: UnfeatureMode, flag: UnfeatureMode): boolean {
return (unfeatureMode & flag) === flag;
}
const modrinthDependencyKinds = new Map([
[DependencyKind.Depends, "required"],
[DependencyKind.Recommends, "optional"],
[DependencyKind.Suggests, "optional"],
[DependencyKind.Includes, "embedded"],
[DependencyKind.Breaks, "incompatible"],
]);
export default class ModrinthPublisher extends ModPublisher {
public get target(): PublisherTarget {
return PublisherTarget.Modrinth;
}
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[], options: Record<string, unknown>): Promise<void> {
const featured = mapBooleanInput(options.featured, true);
const unfeatureMode = mapEnumInput(options.unfeatureMode, UnfeatureMode, featured ? UnfeatureMode.Subset : UnfeatureMode.None);
const projects = (await Promise.all(dependencies
.filter((x, _, self) => (x.kind !== DependencyKind.Suggests && x.kind !== DependencyKind.Includes) || !self.find(y => y.id === x.id && y.kind !== DependencyKind.Suggests && y.kind !== DependencyKind.Includes))
.map(async x => ({
project_id: (await getProject(x.getProjectSlug(this.target)))?.id,
dependency_type: modrinthDependencyKinds.get(x.kind)
}))))
.filter(x => x.project_id && x.dependency_type);
if (unfeatureMode !== UnfeatureMode.None) {
await this.unfeatureOlderVersions(id, token, unfeatureMode, loaders, gameVersions);
}
const data = {
name: name || version,
version_number: version,
changelog,
game_versions: gameVersions,
version_type: channel,
loaders,
featured,
dependencies: projects
};
await createVersion(id, data, files, token);
}
private async unfeatureOlderVersions(id: string, token: string, unfeatureMode: UnfeatureMode, loaders: string[], gameVersions: string[]): Promise<void> {
const unfeaturedVersions = new Array<string>();
const stopwatch = LoggingStopwatch.startNew(this.logger, "📝 Unfeaturing older Modrinth versions...", ms => `✅ Successfully unfeatured: ${unfeaturedVersions.join(", ")} (in ${ms} ms)`);
const versionSubset = hasFlag(unfeatureMode, UnfeatureMode.VersionSubset);
const loaderSubset = hasFlag(unfeatureMode, UnfeatureMode.LoaderSubset);
const olderVersions = await getVersions(id, hasFlag(unfeatureMode, UnfeatureMode.LoaderAny) ? null : loaders, hasFlag(unfeatureMode, UnfeatureMode.VersionAny) ? null : gameVersions, true, token);
for (const olderVersion of olderVersions) {
if (loaderSubset && !olderVersion.loaders.every(x => loaders.includes(x))) {
continue;
}
if (versionSubset && !olderVersion.game_versions.every(x => gameVersions.includes(x))) {
continue;
}
if (await modifyVersion(olderVersion.id, { featured: false }, token)) {
unfeaturedVersions.push(olderVersion.id);
} else {
this.logger.warn(`⚠️ Cannot unfeature version ${olderVersion.id}`);
}
}
if (unfeaturedVersions.length) {
stopwatch.stop();
} else {
this.logger.info("✅ No versions to unfeature were found");
}
}
}