UNPKG

@xmcl/curseforge

Version:

An implementation of curseforge (official) API in https://docs.curseforge.com/

8 lines (7 loc) 32.1 kB
{ "version": 3, "sources": ["../index.ts"], "sourcesContent": ["/* eslint-disable n/no-unsupported-features/node-builtins */\n/**\n * @module @xmcl/curseforge\n */\n\nexport interface ModAsset {\n id: number\n modId: number\n title: string\n description: string\n thumbnailUrl: string\n url: string\n}\n\nexport const enum ModStatus {\n New = 1,\n ChangesRequired = 2,\n UnderSoftReview = 3,\n Approved = 4,\n Rejected = 5,\n ChangesMade = 6,\n Inactive = 7,\n Abandoned = 8,\n Deleted = 9,\n UnderReview = 10,\n}\nexport const enum FileReleaseType {\n Release = 1,\n Beta = 2,\n Alpha = 3,\n}\n\nexport const enum FileModLoaderType {\n Any = 0,\n Forge = 1,\n Cauldron = 2,\n LiteLoader = 3,\n Fabric = 4,\n Quilt = 5,\n NeoForge = 6,\n}\nexport interface FileIndex {\n gameVersion: string\n fileId: number\n filename: string\n releaseType: FileReleaseType\n\n gameVersionTypeId: number | null\n modLoader: FileModLoaderType\n}\n\nexport interface Mod {\n /**\n * The addon id. You can use this in many functions required the `addonID`\n */\n id: number\n /**\n * Game id. Minecraft is 432.\n */\n gameId: number\n /**\n * The display name of the addon\n */\n name: string\n /**\n * The mod slug that would appear in the URL\n */\n slug: string\n /** Relevant links for the mod such as Issue tracker and Wiki */\n links: {\n websiteUrl: string\n wikiUrl: string\n issuesUrl: string\n sourceUrl: string\n }\n /**\n * One line summery\n */\n summary: string\n /**\n * Current mod status\n */\n status: ModStatus\n /**\n * Number of downloads for the mod\n */\n downloadCount: number\n /**\n * Whether the mod is included in the featured mods list\n */\n isFeatured: boolean\n /**\n * The main category of the mod as it was chosen by the mod author\n */\n primaryCategoryId: number\n /**\n * List of categories that this mod is related to\n */\n categories: ModCategory[]\n /**\n * The class id this mod belongs to\n */\n classId: number | null\n /**\n * The list of authors\n */\n authors: Author[]\n\n logo: ModAsset\n\n screenshots: ModAsset[]\n /**\n * The id of the main file of the mod\n */\n mainFileId: number\n latestFiles: File[]\n /**\n * List of file related details for the latest files of the mod\n */\n latestFilesIndexes: FileIndex[]\n /**\n * The creation date of the mod\n */\n dateCreated: string\n\n dateModified: string\n dateReleased: string\n\n /**\n * Is mod allowed to be distributed\n */\n allowModDistribution: boolean | null\n /**\n * The mod popularity rank for the game\n */\n gamePopularityRank: number\n /**\n * Is the mod available for search. This can be false when a mod is experimental, in a deleted state or has only alpha files\n */\n isAvailable: boolean\n\n /**\n * The default download file id\n */\n defaultFileId: number\n /**\n * The mod's thumbs up count\n */\n thumbsUpCount: number\n}\n\nexport interface GameVersionLatestFile {\n gameVersion: string\n projectFileId: number\n projectFileName: string\n fileType: number\n}\n\nexport interface CategorySection {\n id: number\n gameId: number\n name: string\n packageType: number\n path: string\n initialInclusionPattern: string\n extraIncludePattern?: any\n gameCategoryId: number\n}\nexport const enum HashAlgo {\n Sha1 = 1,\n Md5 = 2,\n}\nexport interface FileHash {\n algo: HashAlgo\n value: string\n}\n\nexport const enum FileStatus {\n Processing = 1,\n ChangesRequired = 2,\n UnderReview = 3,\n Approved = 4,\n Rejected = 5,\n MalwareDetected = 6,\n Deleted = 7,\n Archived = 8,\n Testing = 9,\n Released = 10,\n ReadyForReview = 11,\n Deprecated = 12,\n Baking = 13,\n AwaitingPublishing = 14,\n FailedPublishing = 15,\n}\n\nexport const enum FileRelationType {\n EmbeddedLibrary = 1,\n OptionalDependency = 2,\n RequiredDependency = 3,\n Tool = 4,\n Incompatible = 5,\n Include = 6,\n}\n\nexport interface FileDependency {\n modId: number\n relationType: FileRelationType\n}\n\nexport interface File {\n /**\n * The fileID\n */\n id: number\n /**\n * The game id related to the mod that this file belongs to\n */\n gameId: number\n /**\n * The projectId (addonId)\n */\n modId: number\n /**\n * Whether the file is available to download\n */\n isAvailable: boolean\n /**\n * Display name\n */\n displayName: string\n /**\n * File name. Might be the same with `displayName`\n */\n fileName: string\n /**\n * Release or type.\n * - `1` is the release\n * - `2` beta\n * - `3` alpha\n */\n releaseType: number\n\n fileStatus: FileStatus\n\n hashes: FileHash[]\n\n fileFingerprint: number\n\n /**\n * The date of this file uploaded\n */\n fileDate: string\n /**\n * # bytes of this file.\n */\n fileLength: number\n\n /**\n * Number of downloads for the mod\n */\n downloadCount: number\n\n /**\n * Url to download\n */\n downloadUrl?: string\n /**\n * Game version string array, like `[\"1.12.2\"]`\n */\n gameVersions: string[]\n /**\n * Metadata used for sorting by game versions\n */\n isAlternate: boolean\n alternateFileId: number\n dependencies: FileDependency[]\n /**\n * What files inside?\n */\n modules: Module[]\n sortableGameVersions?: SortableGameVersion[]\n}\n\nexport interface SortableGameVersion {\n gameVersionPadded: string\n gameVersion: string\n gameVersionReleaseDate: string\n gameVersionName: string\n}\n\n/**\n * Represent a file in a `File`.\n */\nexport interface Module {\n /**\n * Actually the file name, not the folder\n */\n name: string\n /**\n * A number represent fingerprint\n */\n fingerprint: number\n type: number\n}\n\n/**\n * The author info\n */\nexport interface Author {\n /**\n * The project id of this query\n */\n projectId: number\n projectTitleId?: any\n projectTitleTitle?: any\n\n /**\n * Display name of the author\n */\n name: string\n /**\n * The full url of author homepage in curseforge\n */\n url: string\n /**\n * The id of this author\n */\n id: number\n userId: number\n twitchId: number\n}\n\nexport interface ModCategory {\n /**\n * The category id\n */\n id: number\n gameId: number\n name: string\n slug: string\n url: string\n iconUrl: string\n dateModified: string\n /**\n * A top level category for other categories\n */\n isClass: boolean | null\n /**\n * The class id of the category, meaning - the class of which this category is under\n */\n classId: number | null\n /**\n * The parent category for this category\n */\n parentCategoryId: number | null\n /**\n * The display index for this category\n */\n displayIndex: number | null\n}\n\n/**\n * The search options of the search API.\n *\n * @see {@link searchMods}\n */\nexport interface SearchOptions {\n /**\n * The category section id, which is also a category id.\n * You can fetch if from `getCategories`.\n *\n * To get available categories, you can:\n *\n * ```ts\n * const cat = await getCategories();\n * const sectionIds = cat\n * .filter(c => c.gameId === 432) // 432 is minecraft game id\n * .filter(c => c.rootGameCategoryId === null).map(c => c.id);\n * // the sectionIds is all normal sections here\n * ```\n *\n * @see {@link getCategories}\n */\n classId?: number\n /**\n * This is actually the sub category id of the `sectionId`. All the numbers for this should also be fetch by `getCategories`.\n *\n * To get available values, you can:\n *\n * ```ts\n * const cat = await getCategories();\n * const sectionId = 6; // the mods\n * const categoryIds = cat\n * .filter(c => c.gameId === 432) // 432 is minecraft game id\n * .filter(c => c.rootGameCategoryId === sectionId) // only under the section id\n * .map(c => c.id);\n * // Use categoryIds' id to search under the corresponding section id.\n * ```\n *\n * @see {@link getCategories}\n */\n categoryId?: number\n /**\n * The game id. The Minecraft is 432.\n *\n * @default 432\n */\n gameId?: number\n /**\n * The game version. For Minecraft, it should looks like 1.12.2.\n */\n gameVersion?: string\n /**\n * The index of the addon, NOT the page!\n *\n * When your page size is 25, if you want to get next page contents, you should have index = 25 to get 2nd page content.\n *\n * @default 0\n */\n index?: number\n /**\n * Filter by ModsSearchSortField enumeration\n */\n sortField?: ModsSearchSortField\n /**\n * 'asc' if sort is in ascending order, 'desc' if sort is in descending order\n */\n sortOrder?: 'asc' | 'desc'\n /**\n * Filter only mods associated to a given modloader (Forge, Fabric ...). Must be coupled with gameVersion.\n */\n modLoaderType?: FileModLoaderType\n\n modLoaderTypes?: string[]\n /**\n * Filter only mods that contain files tagged with versions of the given gameVersionTypeId\n */\n gameVersionTypeId?: number\n /**\n * Filter by slug (coupled with classId will result in a unique result).\n */\n slug?: string\n /**\n * The page size, or the number of the addons in a page.\n *\n * @default 25\n */\n pageSize?: number\n /**\n * The keyword of search. If this is absent, it just list out the available addons by `sectionId` and `categoryId`.\n */\n searchFilter?: string\n}\n\nexport const enum ModsSearchSortField {\n Featured = 1,\n Popularity = 2,\n LastUpdated = 3,\n Name = 4,\n Author = 5,\n TotalDownloads = 6,\n Category = 7,\n GameVersion = 8,\n}\n\n/**\n * The options to query\n */\nexport interface QueryOption {\n /**\n * Additional header\n */\n headers?: Record<string, any>\n /**\n * override the http client\n */\n client?: (\n url: string,\n options: QueryOption,\n body?: object,\n text?: boolean,\n ) => Promise<object | string>\n}\n\nexport interface GetModFilesOptions {\n modId: number\n gameVersion?: string\n modLoaderType?: FileModLoaderType\n /**\n * Filter only files that are tagged with versions of the given gameVersionTypeId\n */\n gameVersionTypeId?: number\n index?: number\n pageSize?: number\n}\n\nexport interface Pagination {\n /**\n * A zero based index of the first item that is included in the response\n */\n index: number\n /**\n * The requested number of items to be included in the response\n */\n pageSize: number\n /**\n * The actual number of items that were included in the response\n */\n resultCount: number\n /**\n * The total number of items available by the fetch\n */\n totalCount: number\n}\n\nexport interface CurseforgeClientOptions {\n /**\n * Extra headers\n */\n headers?: Record<string, string>\n /**\n * The base url, the default is `https://api.curseforge.com`\n */\n baseUrl?: string\n /**\n * The fetch function to use. The default is `fetch`\n */\n fetch?: typeof fetch\n}\n\nexport interface FingerprintMatch {\n /**\n * The mod id\n */\n id: number\n file: File\n latestFiles: File[]\n}\nexport interface FingerprintsMatchesResult {\n data: {\n isCacheBuilt: boolean\n exactMatches: FingerprintMatch[]\n exactFingerprints: number[]\n partialMatches: FingerprintMatch[]\n partialFingerprints: object\n unmatchedFingerprints: number[]\n }\n}\n\nexport interface FingerprintFuzzyMatch {\n id: number\n file: File\n latestFiles: File[]\n fingerprints: number[]\n}\n\nexport interface FingerprintFuzzyMatchResult {\n data: {\n fuzzyMatches: FingerprintFuzzyMatch[]\n }\n}\n\nexport class CurseforgeApiError extends Error {\n constructor(\n readonly url: string,\n readonly status: number,\n readonly body: string,\n ) {\n super(`Fail to fetch curseforge api ${url}. Status=${status}. ${body}`)\n this.name = 'CurseforgeApiError'\n }\n}\n\n/**\n * Reference the https://docs.curseforge.com/#curseforge-core-api-mods\n */\nexport class CurseforgeV1Client {\n headers: Record<string, string>\n private fetch: typeof fetch\n private baseUrl: string\n\n constructor(\n private apiKey: string,\n options?: CurseforgeClientOptions,\n ) {\n this.headers = {\n 'x-api-key': this.apiKey,\n ...options?.headers,\n }\n this.baseUrl = options?.baseUrl || 'https://api.curseforge.com'\n this.fetch = options?.fetch || ((...args) => fetch(...args))\n }\n\n /**\n * @see https://docs.curseforge.com/#get-categories\n */\n async getCategories(signal?: AbortSignal) {\n const url = new URL(this.baseUrl + '/v1/categories')\n url.searchParams.append('gameId', '432')\n const response = await this.fetch(url, {\n headers: {\n ...this.headers,\n accept: 'application/json',\n },\n signal,\n })\n if (response.status !== 200) {\n throw new CurseforgeApiError(url.toString(), response.status, await response.text())\n }\n const categories = (await response.json()) as { data: ModCategory[] }\n return categories.data\n }\n\n /**\n * Get the mod by mod Id.\n * @see https://docs.curseforge.com/#get-mod\n * @param modId The id of mod\n * @param options The query options\n */\n async getMod(modId: number, signal?: AbortSignal) {\n const url = new URL(this.baseUrl + `/v1/mods/${modId}`)\n const response = await this.fetch(url, {\n headers: {\n accept: 'application/json',\n ...this.headers,\n },\n signal,\n })\n if (response.status !== 200) {\n throw new CurseforgeApiError(url.toString(), response.status, await response.text())\n }\n const result = (await response.json()) as { data: Mod }\n return result.data\n }\n\n /**\n * @see https://docs.curseforge.com/#get-mod-description\n */\n async getModDescription(modId: number, signal?: AbortSignal) {\n const url = new URL(this.baseUrl + `/v1/mods/${modId}/description`)\n const response = await this.fetch(url, {\n headers: {\n ...this.headers,\n accept: 'application/json',\n },\n signal,\n })\n if (response.status !== 200) {\n throw new CurseforgeApiError(url.toString(), response.status, await response.text())\n }\n const result = (await response.json()) as { data: string }\n return result.data\n }\n\n /**\n * @see https://docs.curseforge.com/#get-mod-files\n */\n async getModFiles(options: GetModFilesOptions, signal?: AbortSignal) {\n const url = new URL(this.baseUrl + `/v1/mods/${options.modId}/files`)\n url.searchParams.append('gameVersion', options.gameVersion ?? '')\n if (options.modLoaderType !== undefined) {\n url.searchParams.append('modLoaderType', options.modLoaderType?.toString() ?? '')\n }\n url.searchParams.append('gameVersionTypeId', options.gameVersionTypeId?.toString() ?? '')\n url.searchParams.append('index', options.index?.toString() ?? '')\n url.searchParams.append('pageSize', options.pageSize?.toString() ?? '')\n const response = await this.fetch(url, {\n headers: {\n ...this.headers,\n accept: 'application/json',\n },\n signal,\n })\n if (response.status !== 200) {\n throw new CurseforgeApiError(url.toString(), response.status, await response.text())\n }\n const result = (await response.json()) as { data: File[]; pagination: Pagination }\n return result\n }\n\n /**\n * @see https://docs.curseforge.com/#curseforge-core-api-files\n */\n async getModFile(modId: number, fileId: number, signal?: AbortSignal) {\n const url = new URL(this.baseUrl + `/v1/mods/${modId}/files/${fileId}`)\n const response = await this.fetch(url, {\n headers: {\n ...this.headers,\n accept: 'application/json',\n },\n signal,\n })\n if (response.status !== 200) {\n throw new CurseforgeApiError(url.toString(), response.status, await response.text())\n }\n const result = (await response.json()) as { data: File }\n return result.data\n }\n\n /**\n * @see https://docs.curseforge.com/#get-mods\n */\n async getMods(modIds: number[], signal?: AbortSignal) {\n const url = new URL(this.baseUrl + '/v1/mods')\n const response = await this.fetch(url, {\n method: 'POST',\n body: JSON.stringify({ modIds }),\n headers: {\n ...this.headers,\n 'content-type': 'application/json',\n accept: 'application/json',\n },\n signal,\n })\n if (response.status !== 200) {\n throw new CurseforgeApiError(url.toString(), response.status, await response.text())\n }\n const result = (await response.json()) as { data: Mod[] }\n return result.data\n }\n\n /**\n * @see https://docs.curseforge.com/#get-files\n */\n async getFiles(fileIds: number[], signal?: AbortSignal) {\n const url = new URL(this.baseUrl + '/v1/mods/files')\n const response = await this.fetch(url, {\n method: 'POST',\n body: JSON.stringify({ fileIds }),\n headers: {\n ...this.headers,\n 'content-type': 'application/json',\n accept: 'application/json',\n },\n signal,\n })\n if (response.status !== 200) {\n throw new CurseforgeApiError(url.toString(), response.status, await response.text())\n }\n const result = (await response.json()) as { data: File[] }\n return result.data\n }\n\n /**\n * @see https://docs.curseforge.com/#search-mods\n */\n async searchMods(options: SearchOptions, signal?: AbortSignal) {\n const url = new URL(this.baseUrl + '/v1/mods/search')\n url.searchParams.append('gameId', '432')\n if (options.classId) {\n url.searchParams.append('classId', options.classId.toString())\n }\n if (options.categoryId) {\n url.searchParams.append('categoryId', options.categoryId.toString())\n }\n if (options.gameVersion) {\n url.searchParams.append('gameVersion', options.gameVersion)\n }\n if (options.searchFilter) {\n url.searchParams.append('searchFilter', options.searchFilter)\n }\n url.searchParams.append(\n 'sortField',\n options.sortField?.toString() ?? ModsSearchSortField.Popularity.toString(),\n )\n url.searchParams.append('sortOrder', options.sortOrder ?? 'desc')\n if (options.modLoaderType) {\n url.searchParams.append('modLoaderType', options.modLoaderType.toString())\n }\n if (options.modLoaderTypes) {\n url.searchParams.append('modLoaderTypes', '[' + options.modLoaderTypes.join(',') + ']')\n }\n if (options.gameVersionTypeId) {\n url.searchParams.append('gameVersionTypeId', options.gameVersionTypeId.toString())\n }\n url.searchParams.append('index', options.index?.toString() ?? '0')\n url.searchParams.append('pageSize', options.pageSize?.toString() ?? '25')\n if (options.slug) {\n url.searchParams.append('slug', options.slug)\n }\n const response = await this.fetch(url, {\n headers: {\n ...this.headers,\n accept: 'application/json',\n },\n signal,\n })\n if (response.status !== 200) {\n throw new CurseforgeApiError(url.toString(), response.status, await response.text())\n }\n const result = (await response.json()) as { data: Mod[]; pagination: Pagination }\n return result\n }\n\n /**\n * https://docs.curseforge.com/#get-mod-file-changelog\n */\n async getModFileChangelog(modId: number, fileId: number, signal?: AbortSignal) {\n const url = new URL(this.baseUrl + `/v1/mods/${modId}/files/${fileId}/changelog`)\n const response = await this.fetch(url, {\n headers: {\n ...this.headers,\n accept: 'application/json',\n },\n signal,\n })\n if (response.status !== 200) {\n throw new CurseforgeApiError(url.toString(), response.status, await response.text())\n }\n const result = (await response.json()) as { data: string }\n return result.data\n }\n\n async getFingerprintsMatchesByGameId(\n gameId: number,\n fingerprints: number[],\n signal?: AbortSignal,\n ) {\n // Drop any non-finite / non-positive entries up front. They come from\n // callers that `parseInt` a non-numeric resource-mapping key, or from\n // an empty mods folder where `Object.keys(fingerprintMap)` is `[]`.\n // Either way CurseForge rejects the request with HTTP 400\n // \"Fingerprints are required.\" (issue #1469 \u2014 234 users in 0.56.4).\n const cleaned = Array.isArray(fingerprints)\n ? fingerprints.filter((n) => Number.isFinite(n) && n > 0)\n : []\n if (cleaned.length === 0) {\n // CurseForge rejects empty arrays with HTTP 400 \"Fingerprints are required.\".\n // Short-circuit instead of burning a request + API quota (see issue #1443).\n const empty: FingerprintsMatchesResult['data'] = {\n isCacheBuilt: false,\n exactMatches: [],\n exactFingerprints: [],\n partialMatches: [],\n partialFingerprints: {},\n unmatchedFingerprints: [],\n }\n return empty\n }\n const url = new URL(this.baseUrl + `/v1/fingerprints/${gameId}`)\n const response = await this.fetch(url, {\n method: 'POST',\n body: JSON.stringify({ fingerprints: cleaned }),\n headers: {\n ...this.headers,\n 'content-type': 'application/json',\n accept: 'application/json',\n },\n signal,\n })\n if (response.status !== 200) {\n throw new CurseforgeApiError(url.toString(), response.status, await response.text())\n }\n const result = (await response.json()) as FingerprintsMatchesResult\n return result.data\n }\n\n async getFingerprintsFuzzyMatchesByGameId(\n gameId: number,\n fingerprints: number[],\n signal?: AbortSignal,\n ) {\n if (!fingerprints || fingerprints.length === 0) {\n const empty: FingerprintsMatchesResult['data'] = {\n isCacheBuilt: false,\n exactMatches: [],\n exactFingerprints: [],\n partialMatches: [],\n partialFingerprints: {},\n unmatchedFingerprints: [],\n }\n return empty\n }\n const url = new URL(this.baseUrl + `/v1/fingerprints/fuzzy/${gameId}`)\n const response = await this.fetch(url, {\n method: 'POST',\n body: JSON.stringify({ fingerprints }),\n headers: {\n ...this.headers,\n 'content-type': 'application/json',\n accept: 'application/json',\n },\n signal,\n })\n if (response.status !== 200) {\n throw new CurseforgeApiError(url.toString(), response.status, await response.text())\n }\n const result = (await response.json()) as FingerprintFuzzyMatchResult\n return result.data\n }\n}\n\nexport function guessCurseforgeFileUrl(id: number, name: string) {\n const fileId = id.toString()\n return [\n `https://edge.forgecdn.net/files/${fileId.slice(0, 4)}/${fileId.slice(4)}/${name}`,\n `https://mediafiles.forgecdn.net/files/${fileId.slice(0, 4)}/${fileId.slice(4)}/${name}`,\n ]\n}\n\nexport function getCurseforgeFileDownloadUrls(id: number, name: string, downloadUrl?: string) {\n return [...new Set([...(downloadUrl ? [downloadUrl] : []), ...guessCurseforgeFileUrl(id, name)])]\n}\n"], "mappings": "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", "names": ["ModStatus", "FileReleaseType", "FileModLoaderType", "HashAlgo", "FileStatus", "FileRelationType", "ModsSearchSortField"] }