mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2024-11-22 08:20:58 -05:00
Modrinth Dependencies (#8)
* temp Modrinth Dependencies * Fix final issues * Got to love git sometimes * Made test case for modrinth-utils * That's why testing is important kids * Included dependencies are still dependencies * Naming * Moved to Modrinth API v2 Co-authored-by: Kir_Antipov <kp.antipov@gmail.com>
This commit is contained in:
parent
347040cd63
commit
bf3f3c7c01
9 changed files with 151 additions and 87 deletions
64
package-lock.json
generated
64
package-lock.json
generated
|
@ -2476,6 +2476,16 @@
|
|||
"integrity": "sha512-V3orv+ggDsWVHP99K3JlwtH20R7J4IhI1Kksgc+64q5VxgfRkQG8Ws3MFm/FZOKDYGy9feGFlZ70/HpCNe9QaA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/node-fetch": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz",
|
||||
"integrity": "sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"@types/prettier": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.3.tgz",
|
||||
|
@ -2855,6 +2865,11 @@
|
|||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||
"dev": true
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"babel-jest": {
|
||||
"version": "28.1.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.0.tgz",
|
||||
|
@ -3170,6 +3185,14 @@
|
|||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
|
||||
"dev": true
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
|
@ -3251,6 +3274,11 @@
|
|||
"object-keys": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"deprecation": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||
|
@ -3697,13 +3725,14 @@
|
|||
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
|
||||
"dev": true
|
||||
},
|
||||
"formdata-node": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.3.2.tgz",
|
||||
"integrity": "sha512-k7lYJyzDOSL6h917favP8j1L0/wNyylzU+x+1w4p5haGVHNlP58dbpdJhiCUsDbWsa9HwEtLp89obQgXl2e0qg==",
|
||||
"form-data": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
|
||||
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
|
||||
"requires": {
|
||||
"node-domexception": "1.0.0",
|
||||
"web-streams-polyfill": "4.0.0-beta.1"
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
|
@ -5578,6 +5607,19 @@
|
|||
"picomatch": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"requires": {
|
||||
"mime-db": "1.52.0"
|
||||
}
|
||||
},
|
||||
"mimic-fn": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||
|
@ -5605,11 +5647,6 @@
|
|||
"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": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
|
@ -6365,11 +6402,6 @@
|
|||
"makeerror": "1.0.12"
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
},
|
||||
"which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
"@babel/preset-env": "^7.18.2",
|
||||
"@babel/preset-typescript": "^7.17.12",
|
||||
"@types/node": "^17.0.36",
|
||||
"@types/node-fetch": "^2.6.1",
|
||||
"@types/yazl": "^2.4.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.0",
|
||||
"@typescript-eslint/parser": "^5.27.0",
|
||||
|
@ -44,7 +45,7 @@
|
|||
"@actions/core": "^1.8.2",
|
||||
"@actions/github": "^5.0.3",
|
||||
"fast-glob": "^3.2.11",
|
||||
"formdata-node": "^4.3.2",
|
||||
"form-data": "^3.0.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"toml": "^3.0.0"
|
||||
|
|
|
@ -1,22 +1,40 @@
|
|||
import { createVersion } from "../../utils/modrinth-utils";
|
||||
import { createVersion, getProject } from "../../utils/modrinth-utils";
|
||||
import { File } from "../../utils/file";
|
||||
import ModPublisher from "../mod-publisher";
|
||||
import PublisherTarget from "../publisher-target";
|
||||
import Dependency from "../../metadata/dependency";
|
||||
import DependencyKind from "../../metadata/dependency-kind";
|
||||
|
||||
const modrinthDependencyKinds = new Map([
|
||||
[DependencyKind.Depends, "required"],
|
||||
[DependencyKind.Recommends, "optional"],
|
||||
[DependencyKind.Suggests, "optional"],
|
||||
[DependencyKind.Includes, "optional"],
|
||||
[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[]): Promise<void> {
|
||||
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> {
|
||||
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);
|
||||
|
||||
const data = {
|
||||
version_title: name || version,
|
||||
name: name || version,
|
||||
version_number: version,
|
||||
version_body: changelog,
|
||||
release_channel: channel,
|
||||
changelog,
|
||||
game_versions: gameVersions,
|
||||
version_type: channel,
|
||||
loaders,
|
||||
featured: true,
|
||||
dependencies: projects
|
||||
};
|
||||
await createVersion(id, data, files, token);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import fetch from "node-fetch";
|
||||
import { FormData } from "formdata-node";
|
||||
import { fileFromPath } from "formdata-node/file-from-path";
|
||||
import FormData from "form-data";
|
||||
import { File } from "./file";
|
||||
import { findVersionByName } from "./minecraft-utils";
|
||||
import SoftError from "./soft-error";
|
||||
|
@ -111,11 +110,12 @@ export async function uploadFile(id: string, data: Record<string, any>, file: Fi
|
|||
}
|
||||
|
||||
const form = new FormData();
|
||||
form.append("file", await fileFromPath(file.path), file.name);
|
||||
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: <any>form
|
||||
});
|
||||
|
||||
|
|
|
@ -11,6 +11,10 @@ export class File {
|
|||
Object.freeze(this);
|
||||
}
|
||||
|
||||
public getStream(): fs.ReadStream {
|
||||
return fs.createReadStream(this.path);
|
||||
}
|
||||
|
||||
public async getBuffer(): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(this.path, (error, data) => {
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import crypto from "crypto";
|
||||
import fs from "fs";
|
||||
|
||||
export function computeHash(path: string, algorithm: string): Promise<crypto.Hash> {
|
||||
const hash = crypto.createHash(algorithm);
|
||||
return new Promise(resolve => fs.createReadStream(path).on("data", data => hash.update(data)).on("end", () => resolve(hash)));
|
||||
}
|
|
@ -1,15 +1,26 @@
|
|||
import { FormData } from "formdata-node";
|
||||
import { fileFromPath } from "formdata-node/file-from-path";
|
||||
import FormData from "form-data";
|
||||
import fetch from "node-fetch";
|
||||
import { File } from "./file";
|
||||
import { computeHash } from "./hash-utils";
|
||||
import SoftError from "./soft-error";
|
||||
|
||||
export async function createVersion(modId: string, data: Record<string, any>, files: File[], token: string): Promise<string> {
|
||||
const baseUrl = "https://api.modrinth.com/v2";
|
||||
|
||||
interface ModrinthProject {
|
||||
id: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
interface ModrinthVersion {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export async function createVersion(modId: string, data: Record<string, any>, files: File[], token: string): Promise<ModrinthVersion> {
|
||||
data = {
|
||||
featured: true,
|
||||
dependencies: [],
|
||||
...data,
|
||||
mod_id: modId,
|
||||
project_id: modId,
|
||||
primary_file: files.length ? "0" : undefined,
|
||||
file_parts: files.map((_, i) => i.toString())
|
||||
};
|
||||
|
||||
|
@ -17,12 +28,14 @@ export async function createVersion(modId: string, data: Record<string, any>, fi
|
|||
form.append("data", JSON.stringify(data));
|
||||
for (let i = 0; i < files.length; ++i) {
|
||||
const file = files[i];
|
||||
form.append(i.toString(), await fileFromPath(file.path), file.name);
|
||||
form.append(i.toString(), file.getStream(), file.name);
|
||||
}
|
||||
|
||||
const response = await fetch("https://api.modrinth.com/api/v1/version", {
|
||||
const response = await fetch(`${baseUrl}/version`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: token },
|
||||
headers: form.getHeaders({
|
||||
Authorization: token,
|
||||
}),
|
||||
body: <any>form
|
||||
});
|
||||
|
||||
|
@ -35,27 +48,19 @@ export async function createVersion(modId: string, data: Record<string, any>, fi
|
|||
throw new SoftError(isServerError, `Failed to upload file: ${response.status} (${errorText})`);
|
||||
}
|
||||
|
||||
const versionId = (<{ id: string }>await response.json()).id;
|
||||
const primaryFile = files[0];
|
||||
if (primaryFile) {
|
||||
await makeFilePrimary(versionId, primaryFile.path, token);
|
||||
}
|
||||
return versionId;
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function makeFilePrimary(versionId: string, filePath: string, token: string): Promise<boolean> {
|
||||
const algorithm = "sha1";
|
||||
const hash = (await computeHash(filePath, algorithm)).digest("hex");
|
||||
|
||||
const response = await fetch(`https://api.modrinth.com/api/v1/version/${versionId}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Authorization": token,
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
primary_file: [algorithm, hash]
|
||||
})
|
||||
});
|
||||
return response.ok;
|
||||
export async function getProject(idOrSlug: string): Promise<ModrinthProject> {
|
||||
const response = await fetch(`${baseUrl}/project/${idOrSlug}`);
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isServerError = response.status >= 500;
|
||||
throw new SoftError(isServerError, `${response.status} (${response.statusText})`);
|
||||
}
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import { describe, test, expect, beforeAll, afterAll } from "@jest/globals";
|
||||
import { computeHash } from "../src/utils/hash-utils";
|
||||
import fs from "fs";
|
||||
|
||||
describe("computeHash", () => {
|
||||
beforeAll(() => new Promise(resolve => {
|
||||
fs.writeFile("hello-world.txt", "Hello world!", resolve);
|
||||
}));
|
||||
|
||||
afterAll(() => new Promise(resolve => fs.unlink("hello-world.txt", resolve)));
|
||||
|
||||
test("sha1 is supported", async () => {
|
||||
const algorithm = "sha1";
|
||||
expect((await computeHash("hello-world.txt", algorithm)).digest("hex")).toBe("d3486ae9136e7856bc42212385ea797094475802");
|
||||
});
|
||||
|
||||
test("sha256 is supported", async () => {
|
||||
const algorithm = "sha256";
|
||||
expect((await computeHash("hello-world.txt", algorithm)).digest("hex")).toBe("c0535e4be2b79ffd93291305436bf889314e4a3faec05ecffcbb7df31ad9e51a");
|
||||
});
|
||||
|
||||
test("sha512 is supported", async () => {
|
||||
const algorithm = "sha512";
|
||||
expect((await computeHash("hello-world.txt", algorithm)).digest("hex")).toBe("f6cde2a0f819314cdde55fc227d8d7dae3d28cc556222a0a8ad66d91ccad4aad6094f517a2182360c9aacf6a3dc323162cb6fd8cdffedb0fe038f55e85ffb5b6");
|
||||
});
|
||||
});
|
37
test/modrinth-utils.test.ts
Normal file
37
test/modrinth-utils.test.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { jest, describe, test, expect } from "@jest/globals";
|
||||
import { getProject } from "../src/utils/modrinth-utils";
|
||||
|
||||
describe("getProject", () => {
|
||||
test("returned versions have expected ids", async () => {
|
||||
jest.setTimeout(15000);
|
||||
const projects = {
|
||||
"sodium": "AANobbMI",
|
||||
"fabric-api": "P7dR8mSH",
|
||||
"sync-fabric": "OrJTMhHF",
|
||||
"nether-chest": "okOUGirG",
|
||||
"ebe": "OVuFYfre",
|
||||
};
|
||||
|
||||
for (const [slug, id] of Object.entries(projects)) {
|
||||
const project = await getProject(slug);
|
||||
expect(project).toHaveProperty("id", id);
|
||||
}
|
||||
});
|
||||
|
||||
test("the method returns null if project with the given slug does not exist", async () => {
|
||||
jest.setTimeout(15000);
|
||||
const nonExistentProjects = [
|
||||
"Na-11",
|
||||
"api-fabric",
|
||||
"sync-forge",
|
||||
"ever-chest",
|
||||
"beb",
|
||||
"i-swear-to-god-if-someone-registers-these-mods"
|
||||
];
|
||||
|
||||
for (const slug of nonExistentProjects) {
|
||||
const project = await getProject(slug);
|
||||
expect(project).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue