@xmcl/modrinth
Version:
An implementation of modrinth API (https://docs.modrinth.com/api-spec)
480 lines (449 loc) • 14.9 kB
text/typescript
/* eslint-disable n/no-unsupported-features/node-builtins */
/**
* @module @xmcl/modrinth
*/
import { Category, GameVersion, License, Loader, Project, ProjectVersion, TeamMember, User } from './types'
export * from './types'
/* eslint-disable camelcase */
export interface SearchResultHit {
/**
* The slug of project, e.g. "my_project"
*/
slug: string
/**
* The id of the project; prefixed with local-
*/
project_id: string
/**
* The project type of the project.
* @enum "mod" "modpack"
* */
project_type: string
/**
* The username of the author of the project
*/
author: string
/**
* The name of the project.
*/
title: string
/**
* A short description of the project
*/
description: string
/**
* A list of the categories the project is in.
*/
categories: Array<string>
/**
* A list of the minecraft versions supported by the project.
*/
versions: Array<string>
/**
* The total number of downloads for the project
*/
downloads: number
follows: number
/**
* A link to the project's main page; */
page_url: string
/**
* The url of the project's icon */
icon_url: string
/**
* The url of the project's author */
author_url: string
/**
* The date that the project was originally created
*/
date_created: string
/**
* The date that the project was last modified
*/
date_modified: string
/**
* The latest version of minecraft that this project supports */
latest_version: string
/**
* The id of the license this project follows */
license: string
/**
* The side type id that this project is on the client */
client_side: string
/**
* The side type id that this project is on the server */
server_side: string
/**
* The host that this project is from, always modrinth */
host: string
gallery: string[]
featured_gallery: string
monetization_status: string
}
export interface SearchProjectOptions {
/**
* The query to search
*/
query?: string
/**
* The recommended way of filtering search results. [Learn more about using facets](https://docs.modrinth.com/docs/tutorials/search).
*
* @enum "categories" "versions" "license" "project_type"
* @example [["categories:forge"],["versions:1.17.1"],["project_type:mod"]]
*/
facets?: string
/**
* A list of filters relating to the properties of a project. Use filters when there isn't an available facet for your needs. [More information](https://docs.meilisearch.com/reference/features/filtering.html)
*
* @example filter=categories="fabric" AND (categories="technology" OR categories="utility")
*/
filter?: string
/**
* What the results are sorted by
*
* @enum "relevance" "downloads" "follows" "newest" "updated"
* @example "downloads"
* @default relevance
*/
index?: string
/**
* The offset into the search; skips this number of results
* @default 0
*/
offset?: number
/**
* The number of mods returned by the search
* @default 10
*/
limit?: number
}
export interface SearchResult {
/**
* The list of results
*/
hits: Array<SearchResultHit>
/**
* The number of results that were skipped by the query
*/
offset: number
/**
* The number of mods returned by the query
*/
limit: number
/**
* The total number of mods that the query found
*/
total_hits: number
}
export interface GetProjectVersionsOptions {
id: string
loaders?: Array<string>
/**
* Minecraft version filtering
*/
game_versions?: Array<string>
featured?: boolean
}
export class ModerinthApiError extends Error {
constructor(readonly url: string, readonly status: number, readonly body: string) {
super(`Fail to fetch modrinth api ${url}. Status=${status}. ${body}`)
this.name = 'ModerinthApiError'
}
}
export interface ModrinthClientOptions {
baseUrl?: string
/**
* The extra headers
*/
headers?: Record<string, string>
/**
* The fetch function to use
*/
fetch?: typeof fetch
}
/**
* @see https://docs.modrinth.com/api-spec
*/
export class ModrinthV2Client {
private baseUrl: string
private fetch: typeof fetch
headers: Record<string, string>
constructor(options?: ModrinthClientOptions) {
this.baseUrl = options?.baseUrl ?? 'https://api.modrinth.com'
this.headers = options?.headers || {}
this.fetch = options?.fetch || ((...args) => fetch(...args))
}
/**
* @see https://docs.modrinth.com/#tag/projects/operation/searchProjects
*/
async searchProjects(options: SearchProjectOptions, signal?: AbortSignal): Promise<SearchResult> {
const url = new URL(this.baseUrl + '/v2/search')
url.searchParams.append('query', options.query || '')
url.searchParams.append('filter', options.filter || '')
url.searchParams.append('index', options.index || (options.query ? 'relevance' : 'downloads'))
url.searchParams.append('offset', options.offset?.toString() ?? '0')
url.searchParams.append('limit', options.limit?.toString() ?? '10')
if (options.facets) { url.searchParams.append('facets', options.facets) }
const response = await this.fetch(url, {
signal,
headers: this.headers,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const result = await response.json() as SearchResult
return result
}
/**
* @see https://docs.modrinth.com/#tag/projects/operation/getProject
*/
async getProject(projectId: string, signal?: AbortSignal): Promise<Project> {
if (projectId.startsWith('local-')) { projectId = projectId.slice('local-'.length) }
const url = new URL(this.baseUrl + `/v2/project/${projectId}`)
const response = await this.fetch(url, {
signal,
headers: this.headers,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const project = await response.json() as Project
return project
}
/**
* @see https://docs.modrinth.com/#tag/projects/operation/getProject
*/
async getProjects(projectIds: string[], signal?: AbortSignal): Promise<Project[]> {
const url = new URL(this.baseUrl + '/v2/projects')
url.searchParams.append('ids', JSON.stringify(projectIds))
const response = await this.fetch(url, {
signal,
headers: this.headers,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const project = await response.json() as Project[]
return project
}
/**
* @see https://docs.modrinth.com/#tag/versions/operation/getProjectVersions
*/
async getProjectVersions(projectId: string, { loaders, gameVersions, featured }: { loaders?: string[]; gameVersions?: string[]; featured?: boolean } = {}, signal?: AbortSignal): Promise<ProjectVersion[]> {
const url = new URL(this.baseUrl + `/v2/project/${projectId}/version`)
if (loaders) { url.searchParams.append('loaders', JSON.stringify(loaders)) }
if (gameVersions) { url.searchParams.append('game_versions', JSON.stringify(gameVersions)) }
if (featured !== undefined) { url.searchParams.append('featured', featured ? 'true' : 'false') }
const response = await this.fetch(url, {
signal,
headers: this.headers,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const versions = await response.json() as ProjectVersion[]
return versions
}
/**
* @see https://docs.modrinth.com/#tag/versions/operation/getVersion
*/
async getProjectVersion(versionId: string, signal?: AbortSignal): Promise<ProjectVersion> {
const url = new URL(this.baseUrl + `/v2/version/${versionId}`)
const response = await this.fetch(url, {
signal,
headers: this.headers,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const version = await response.json() as ProjectVersion
return version
}
/**
* @see https://docs.modrinth.com/#tag/versions/operation/getVersions
*/
async getProjectVersionsById(ids: string[], signal?: AbortSignal) {
const url = new URL(this.baseUrl + '/v2/versions')
url.searchParams.append('ids', JSON.stringify(ids))
const response = await this.fetch(url, {
signal,
headers: this.headers,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const versions = await response.json() as ProjectVersion[]
return versions
}
/**
* @see https://docs.modrinth.com/#tag/version-files/operation/versionsFromHashes
*/
async getProjectVersionsByHash(hashes: string[], algorithm = 'sha1', signal?: AbortSignal) {
const url = new URL(this.baseUrl + '/v2/version_files')
const response = await this.fetch(url, {
method: 'POST',
body: JSON.stringify({
hashes,
algorithm,
}),
headers: {
...this.headers,
'content-type': 'application/json',
},
signal,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const versions = await response.json() as Record<string, ProjectVersion>
return versions
}
/**
* @see https://docs.modrinth.com/api-spec#tag/version-files/operation/getLatestVersionsFromHashes
*/
async getLatestVersionsFromHashes(hashes: string[], { algorithm, loaders = [], gameVersions = [] }: { algorithm?: string; loaders?: string[]; gameVersions?: string[] } = {}, signal?: AbortSignal) {
const url = new URL(this.baseUrl + '/v2/version_files/update')
const response = await this.fetch(url, {
method: 'POST',
body: JSON.stringify({
hashes,
algorithm,
loaders,
game_versions: gameVersions,
}),
headers: { ...this.headers, 'content-type': 'application/json' },
signal,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const versions = await response.json() as Record<string, ProjectVersion>
return versions
}
/**
* @see https://docs.modrinth.com/#tag/version-files/operation/getLatestVersionFromHash
*/
async getLatestProjectVersion(sha1: string, { algorithm, loaders = [], gameVersions = [] }: { algorithm?: string; loaders?: string[]; gameVersions?: string[] } = {}, signal?: AbortSignal): Promise<ProjectVersion> {
const url = new URL(this.baseUrl + `/v2/version_file/${sha1}/update`)
url.searchParams.append('algorithm', algorithm ?? 'sha1')
const response = await this.fetch(url, {
method: 'POST',
body: JSON.stringify({
loaders,
game_versions: gameVersions,
}),
headers: { ...this.headers, 'content-type': 'application/json' },
signal,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const version = await response.json() as ProjectVersion
return version
}
/**
* @see https://docs.modrinth.com/#tag/tags/operation/licenseList
*/
async getLicenseTags(signal?: AbortSignal) {
const url = new URL(this.baseUrl + '/v2/tag/license')
const response = await this.fetch(url, {
headers: this.headers,
signal,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const result = await response.json() as License[]
return result
}
/**
* @see https://docs.modrinth.com/#tag/tags/operation/categoryList
*/
async getCategoryTags(signal?: AbortSignal) {
const url = new URL(this.baseUrl + '/v2/tag/category')
const response = await this.fetch(url, {
headers: this.headers,
signal,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const result = await response.json() as Category[]
return result
}
/**
* @see https://docs.modrinth.com/#tag/tags/operation/versionList
*/
async getGameVersionTags(signal?: AbortSignal) {
const url = new URL(this.baseUrl + '/v2/tag/game_version')
const response = await this.fetch(url, {
headers: this.headers,
signal,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const result = await response.json() as GameVersion[]
return result
}
/**
* @see https://docs.modrinth.com/#tag/tags/operation/loaderList
*/
async getLoaderTags(signal?: AbortSignal) {
const url = new URL(this.baseUrl + '/v2/tag/loader')
const response = await this.fetch(url, {
headers: this.headers,
signal,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const result = await response.json() as Loader[]
return result
}
/**
* @see https://docs.modrinth.com/#tag/teams/operation/getProjectTeamMembers
*/
async getProjectTeamMembers(projectId: string, signal?: AbortSignal) {
const url = new URL(this.baseUrl + `/v2/project/${projectId}/members`)
const response = await this.fetch(url, {
headers: this.headers,
signal,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const result = await response.json() as TeamMember[]
return result
}
/**
* @see https://docs.modrinth.com/#tag/users/operation/getUser
*/
async getUser(id: string, signal?: AbortSignal) {
const url = new URL(this.baseUrl + `/v2/user/${id}`)
const response = await this.fetch(url, {
headers: this.headers,
signal,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const result = await response.json() as User
return result
}
/**
* @see https://docs.modrinth.com/#tag/users/operation/getUserProjects
*/
async getUserProjects(id: string, signal?: AbortSignal) {
const url = new URL(this.baseUrl + `/v2/user/${id}/projects`)
const response = await this.fetch(url, {
headers: this.headers,
signal,
})
if (response.status !== 200) {
throw new ModerinthApiError(url.toString(), response.status, await response.text())
}
const result = await response.json() as Project[]
return result
}
}