Skip to content

Commit

Permalink
Modrinth Dependencies (#8)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
FxMorin and Kir-Antipov authored Jun 5, 2022
1 parent 347040c commit bf3f3c7
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 88 deletions.
64 changes: 48 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
Expand Down
30 changes: 24 additions & 6 deletions src/publishing/modrinth/modrinth-publisher.ts
Original file line number Diff line number Diff line change
@@ -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);
}
Expand Down
6 changes: 3 additions & 3 deletions src/utils/curseforge-utils.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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
});

Expand Down
4 changes: 4 additions & 0 deletions src/utils/file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
7 changes: 0 additions & 7 deletions src/utils/hash-utils.ts

This file was deleted.

63 changes: 34 additions & 29 deletions src/utils/modrinth-utils.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,41 @@
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())
};

const form = new FormData();
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
});

Expand All @@ -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})`);
}
26 changes: 0 additions & 26 deletions test/hast-utils.test.ts

This file was deleted.

37 changes: 37 additions & 0 deletions test/modrinth-utils.test.ts
Original file line number Diff line number Diff line change
@@ -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();
}
});
});

0 comments on commit bf3f3c7

Please sign in to comment.