Compare commits

...

4 Commits

Author SHA1 Message Date
KallumJ 2bd38816ad
Update install script 2024-04-06 19:51:31 +01:00
KallumJ 21691f37db
Add Forgejo as a mod source 2024-04-06 19:50:52 +01:00
KallumJ f7649e2aeb
Add properties parser 2024-04-06 15:27:53 +01:00
KallumJ 2d1513366c
Bump typescript version 2024-04-06 15:27:26 +01:00
7 changed files with 394 additions and 19 deletions

View File

@ -77,6 +77,6 @@ The following are the required environment variables for running Mod Manager
Mod Manager can be installed with the following command (watch out for the sudo password prompt, it can be hard to spot)
```bash
curl https://hogwarts.bits.team/git/Bits/mod-manager/raw/branch/master/install.sh | sudo -E env "PATH=$PATH" bash
curl https://git.bits.team/Bits/mod-manager/raw/branch/master/install.sh | sudo -E env "PATH=$PATH" bash
```

View File

@ -62,7 +62,7 @@ fi
# Download source files
info "Downloading mod-manager source..."
git clone "https://hogwarts.bits.team/git/Bits/mod-manager.git" "$DOWNLOAD_DIR" || exit
git clone "https://git.bits.team/Bits/mod-manager.git" "$DOWNLOAD_DIR" || exit
# Compile
info "Compiling..."

75
package-lock.json generated
View File

@ -10,6 +10,7 @@
"license": "ISC",
"dependencies": {
"@types/inquirer": "^9.0.0",
"@types/properties-parser": "^0.3.3",
"as-table": "^1.0.55",
"axios": "^0.27.2",
"chalk": "^5.0.1",
@ -18,13 +19,14 @@
"node-downloader-helper": "^2.1.2",
"ora": "^6.1.2",
"pino": "^8.3.1",
"properties-parser": "^0.6.0",
"string-format": "^2.0.0",
"string-similarity-js": "^2.1.4",
"typescript-string-operations": "^1.4.1"
},
"devDependencies": {
"@types/node": "^18.6.3",
"typescript": "^4.7.4"
"@types/node": "^18.19.30",
"typescript": "^4.9.5"
}
},
"node_modules/@types/inquirer": {
@ -37,9 +39,20 @@
}
},
"node_modules/@types/node": {
"version": "18.6.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.3.tgz",
"integrity": "sha512-6qKpDtoaYLM+5+AFChLhHermMQxc3TOEFIDzrZLPRGHPrLEwqFkkT5Kx3ju05g6X7uDPazz3jHbKPX0KzCjntg=="
"version": "18.19.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.30.tgz",
"integrity": "sha512-453z1zPuJLVDbyahaa1sSD5C2sht6ZpHp5rgJNs+H8YGqhluCXcuOUmBYsAo0Tos0cHySJ3lVUGbGgLlqIkpyg==",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/properties-parser": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/properties-parser/-/properties-parser-0.3.3.tgz",
"integrity": "sha512-VZhGpE+QQ2JNbaY4B4Y2iM/jdUsq3HO75uBKLk07VT6P2Kg1aifeYL6I3RosFniSdAb4PtuH5UaY8jXU7JeIYA==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/through": {
"version": "0.0.30",
@ -617,6 +630,14 @@
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.0.0.tgz",
"integrity": "sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww=="
},
"node_modules/properties-parser": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/properties-parser/-/properties-parser-0.6.0.tgz",
"integrity": "sha512-qvr2cSmoA0dln0MARAKwBzPkkXn7FqwX+RVVNpMdMJc7rt9mqO2cXwluxtux9fHrLhjnPFaQkS8BM0kFrTCnSw==",
"engines": {
"node": ">= 0.3.1"
}
},
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
@ -824,9 +845,9 @@
}
},
"node_modules/typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@ -841,6 +862,11 @@
"resolved": "https://registry.npmjs.org/typescript-string-operations/-/typescript-string-operations-1.4.1.tgz",
"integrity": "sha512-c+q+Tb0hxeebohdT9KpGUAm5zwxhU8pHeNOeuLCGFMXKN0OrldoAxtufrGLR3xSPCXDA4A3IBCEdRNNscVqLQg=="
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -882,9 +908,20 @@
}
},
"@types/node": {
"version": "18.6.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.3.tgz",
"integrity": "sha512-6qKpDtoaYLM+5+AFChLhHermMQxc3TOEFIDzrZLPRGHPrLEwqFkkT5Kx3ju05g6X7uDPazz3jHbKPX0KzCjntg=="
"version": "18.19.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.30.tgz",
"integrity": "sha512-453z1zPuJLVDbyahaa1sSD5C2sht6ZpHp5rgJNs+H8YGqhluCXcuOUmBYsAo0Tos0cHySJ3lVUGbGgLlqIkpyg==",
"requires": {
"undici-types": "~5.26.4"
}
},
"@types/properties-parser": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/properties-parser/-/properties-parser-0.3.3.tgz",
"integrity": "sha512-VZhGpE+QQ2JNbaY4B4Y2iM/jdUsq3HO75uBKLk07VT6P2Kg1aifeYL6I3RosFniSdAb4PtuH5UaY8jXU7JeIYA==",
"requires": {
"@types/node": "*"
}
},
"@types/through": {
"version": "0.0.30",
@ -1267,6 +1304,11 @@
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.0.0.tgz",
"integrity": "sha512-+MmoAXoUX+VTHAlwns0h+kFUWFs/3FZy+ZuchkgjyOu3oioLAo2LB5aCfKPh2+P9O18i3m43tUEv3YqttSy0Ww=="
},
"properties-parser": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/properties-parser/-/properties-parser-0.6.0.tgz",
"integrity": "sha512-qvr2cSmoA0dln0MARAKwBzPkkXn7FqwX+RVVNpMdMJc7rt9mqO2cXwluxtux9fHrLhjnPFaQkS8BM0kFrTCnSw=="
},
"quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
@ -1417,9 +1459,9 @@
"integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="
},
"typescript": {
"version": "4.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true
},
"typescript-string-operations": {
@ -1427,6 +1469,11 @@
"resolved": "https://registry.npmjs.org/typescript-string-operations/-/typescript-string-operations-1.4.1.tgz",
"integrity": "sha512-c+q+Tb0hxeebohdT9KpGUAm5zwxhU8pHeNOeuLCGFMXKN0OrldoAxtufrGLR3xSPCXDA4A3IBCEdRNNscVqLQg=="
},
"undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@ -5,13 +5,16 @@
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "npx tsc",
"dev": "node ./build/ts/mod-manager.js",
"debug": "npm run build && npm run dev test"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@types/inquirer": "^9.0.0",
"@types/properties-parser": "^0.3.3",
"as-table": "^1.0.55",
"axios": "^0.27.2",
"chalk": "^5.0.1",
@ -20,12 +23,13 @@
"node-downloader-helper": "^2.1.2",
"ora": "^6.1.2",
"pino": "^8.3.1",
"properties-parser": "^0.6.0",
"string-format": "^2.0.0",
"string-similarity-js": "^2.1.4",
"typescript-string-operations": "^1.4.1"
},
"devDependencies": {
"@types/node": "^18.6.3",
"typescript": "^4.7.4"
"@types/node": "^18.19.30",
"typescript": "^4.9.5"
}
}

View File

@ -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();

View File

@ -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<string> {
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<void> {
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<string> {
const response = await this.makeRequest(format(ForgejoSource.REPO_URL, id))
return response.name;
}
async getLatestVersion(id: string = "34", mcVersion: string = "24w13a"): Promise<Version> {
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;
}
}

165
src/types/forgejo.d.ts vendored Normal file
View File

@ -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
}