From 21691f37db3d0f66289f47731b0981d36de4d5a9 Mon Sep 17 00:00:00 2001 From: KallumJ <57623845+KallumJ@users.noreply.github.com> Date: Sat, 6 Apr 2024 19:50:52 +0100 Subject: [PATCH] Add Forgejo as a mod source --- src/mod-manager.ts | 2 + src/mods/sources/forgejo_source.ts | 157 +++++++++++++++++++++++++++ src/types/forgejo.d.ts | 165 +++++++++++++++++++++++++++++ 3 files changed, 324 insertions(+) create mode 100644 src/mods/sources/forgejo_source.ts create mode 100644 src/types/forgejo.d.ts diff --git a/src/mod-manager.ts b/src/mod-manager.ts index 5241250..599b9ec 100644 --- a/src/mod-manager.ts +++ b/src/mod-manager.ts @@ -19,6 +19,7 @@ import Mods from "./mods/mods.js"; import {CurseforgeSource} from "./mods/sources/curseforge_source.js"; import MinecraftUtils from "./util/minecraft_utils.js"; import chalk from "chalk"; +import ForgejoSource from "./mods/sources/forgejo_source.js"; export default class ModManager { public static logger: Logger | null = null; @@ -63,6 +64,7 @@ export default class ModManager { Mods.registerSource(new ModrinthSource()) Mods.registerSource(new CurseforgeSource(), "CURSEFORGE_API_KEY") + Mods.registerSource(new ForgejoSource(), "FORGEJO_API_KEY") this.program.showSuggestionAfterError(); this.program.showHelpAfterError(); diff --git a/src/mods/sources/forgejo_source.ts b/src/mods/sources/forgejo_source.ts new file mode 100644 index 0000000..9a62bcd --- /dev/null +++ b/src/mods/sources/forgejo_source.ts @@ -0,0 +1,157 @@ +import axios from "axios"; +import ModSource from "./mod_source.js"; +import Util from "../../util/util.js"; +import { ForgejoFile, ForgejoFiles, ForgejoPackage as ForgejoSearchResponse } from "../../types/forgejo.js"; +import { parse } from "properties-parser" +import ModNotFoundError from "../../errors/mod_not_found_error.js"; +import MinecraftUtils from "../../util/minecraft_utils.js"; +import { format } from "util"; +import Mods from "../mods.js"; +import FileDownloader from "../../io/file_downloder.js"; +import DownloadError from "../../errors/download_error.js"; + +export default class ForgejoSource implements ModSource { + private static readonly BASE_URL: string = "https://git.bits.team/api/v1"; + private static readonly SEARCH_URL: string = ForgejoSource.BASE_URL + "/packages/Bits" + private static readonly VERSION_URL: string = ForgejoSource.BASE_URL + "/repos/%s/%s/media/gradle.properties" + private static readonly REPO_URL: string = ForgejoSource.BASE_URL + "/repositories/%s" + private static readonly FILES_URL: string = ForgejoSource.BASE_URL + "/packages/%s/%s/%s/%s/files" + + private async findPackage(query: string, mcVersion: string): Promise<{package: ForgejoSearchResponse, project_id: string} | undefined> { + let page = 1; + let pagesLeft = true; + while (pagesLeft) { + pagesLeft = false + + const params = { + q: query, + page + } + const response: ForgejoSearchResponse[] = await this.makeRequest(ForgejoSource.SEARCH_URL, params); + + for (const mod of response) { + const versionParams = { + ref: mod.version + } + + const url = format(ForgejoSource.VERSION_URL, mod.owner.username, mod.repository.name) + + const versionResponse = await this.makeRequest(url, versionParams).catch(_ => ""); + const ver = parse(versionResponse) + + if (ver["minecraft_version"] == mcVersion) { + return { + package: mod, + project_id: mod.repository.id.toString() + } + } + } + + if (!Util.isArrayEmpty(response)) { + pagesLeft = true; + } + + page++ + } + + return undefined; + } + + async search(query: string = "BitsVanilla"): Promise { + const mcVersion = await MinecraftUtils.getCurrentMinecraftVersion(); + + let mod = await this.findPackage(query, mcVersion); + + if (!mod) { + throw new ModNotFoundError(`Mod ${query} could not be found on Forgejo`) + } + + return mod.project_id; + } + async install(version: Version, essential: boolean): Promise { + try { + if (Mods.isModInstalled(version.modId)) { + return; + } + + await FileDownloader.downloadMod(version) + + const mod = { + name: await this.getProjectName(version.modId), + id: version.modId, + fileName: version.fileName, + version: version.versionNumber, + source: this.getSourceName(), + essential: essential, + dependencies: [] + } + + Mods.trackMod(mod) + } catch (e) { + throw new DownloadError(`An error occured downloading mod with id ${version.modId} from ${this.getSourceName()}`) + } + } + getSourceName(): string { + return "Forgejo"; + } + async getProjectName(id: string): Promise { + const response = await this.makeRequest(format(ForgejoSource.REPO_URL, id)) + return response.name; + } + async getLatestVersion(id: string = "34", mcVersion: string = "24w13a"): Promise { + const projectName = await this.getProjectName(id) + + const mod = await this.findPackage(projectName, mcVersion) + + if (!mod) { + throw new ModNotFoundError(`Mod with id ${id} has no available versions on ${this.getSourceName()} for Minecraft version ${mcVersion}`); + } + + let filesUrl = format(ForgejoSource.FILES_URL, mod.package.owner.username, mod.package.type, mod.package.name, mod.package.version) + + let filesResponse: ForgejoFiles = await this.makeRequest(filesUrl); + + if (Util.isArrayEmpty(filesResponse)) { + throw new ModNotFoundError(`Mod ${mod.package.name} has no files on Forgejo`) + } + + let latestFile: ForgejoFile | undefined = undefined; + for (const file of filesResponse) { + if (file.name.endsWith(".jar")) { + latestFile = file + break; + } + } + + if (!latestFile) { + throw new ModNotFoundError(`Mod ${mod.package.name} has no jar files on Forgejo`) + } + + const downloadUrl = mod.package.html_url + `/files/${latestFile.id}` + + const version = { + modId: mod.project_id, + versionNumber: mod.package.version, + fileName: latestFile.name, + url: downloadUrl, + dependencies: [], + checksum: latestFile.sha1 + } + + return version + } + + private async makeRequest(url: string, params?: object) { + if (process.env.FORGEJO_API_KEY == undefined) { + throw new Error("Attempted Forgejo api calls with undefined api key environment variable (FORGEJO_API_KEY)") + } + + params = Object.assign({}, {"access_token": process.env.FORGEJO_API_KEY}, params) + + const response = await axios.get(url, { + params + }) + + return await response.data; + } +} \ No newline at end of file diff --git a/src/types/forgejo.d.ts b/src/types/forgejo.d.ts new file mode 100644 index 0000000..efead7f --- /dev/null +++ b/src/types/forgejo.d.ts @@ -0,0 +1,165 @@ +export interface ForgejoPackage { + id: number + owner: Owner + repository: Repository + creator: Creator + type: string + name: string + version: string + html_url: string + created_at: string + } + + export interface Owner { + id: number + login: string + login_name: string + full_name: string + email: string + avatar_url: string + language: string + is_admin: boolean + last_login: string + created: string + restricted: boolean + active: boolean + prohibit_login: boolean + location: string + website: string + description: string + visibility: string + followers_count: number + following_count: number + starred_repos_count: number + username: string + } + + export interface Repository { + id: number + owner: Owner2 + name: string + full_name: string + description: string + empty: boolean + private: boolean + fork: boolean + template: boolean + parent: any + mirror: boolean + size: number + language: string + languages_url: string + html_url: string + url: string + link: string + ssh_url: string + clone_url: string + original_url: string + website: string + stars_count: number + forks_count: number + watchers_count: number + open_issues_count: number + open_pr_counter: number + release_counter: number + default_branch: string + archived: boolean + created_at: string + updated_at: string + archived_at: string + permissions: Permissions + has_issues: boolean + internal_tracker: InternalTracker + has_wiki: boolean + has_pull_requests: boolean + has_projects: boolean + has_releases: boolean + has_packages: boolean + has_actions: boolean + ignore_whitespace_conflicts: boolean + allow_merge_commits: boolean + allow_rebase: boolean + allow_rebase_explicit: boolean + allow_squash_merge: boolean + allow_rebase_update: boolean + default_delete_branch_after_merge: boolean + default_merge_style: string + default_allow_maintainer_edit: boolean + avatar_url: string + internal: boolean + mirror_interval: string + mirror_updated: string + repo_transfer: any + } + + export interface Owner2 { + id: number + login: string + login_name: string + full_name: string + email: string + avatar_url: string + language: string + is_admin: boolean + last_login: string + created: string + restricted: boolean + active: boolean + prohibit_login: boolean + location: string + website: string + description: string + visibility: string + followers_count: number + following_count: number + starred_repos_count: number + username: string + } + + export interface Permissions { + admin: boolean + push: boolean + pull: boolean + } + + export interface InternalTracker { + enable_time_tracker: boolean + allow_only_contributors_to_track_time: boolean + enable_issue_dependencies: boolean + } + + export interface Creator { + id: number + login: string + login_name: string + full_name: string + email: string + avatar_url: string + language: string + is_admin: boolean + last_login: string + created: string + restricted: boolean + active: boolean + prohibit_login: boolean + location: string + website: string + description: string + visibility: string + followers_count: number + following_count: number + starred_repos_count: number + username: string + } + +export type ForgejoFiles = ForgejoFile[] + +export interface ForgejoFile { + id: number + Size: number + name: string + md5: string + sha1: string + sha256: string + sha512: string +}