UNPKG

enka-network-api

Version:

Enka-network API wrapper for Genshin Impact.

429 lines (428 loc) 19.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.EnkaClient = exports.defaultEnkaClientOptions = exports.defaultImageBaseUrls = void 0; const GenshinUser_1 = require("../models/GenshinUser"); const characterUtils = __importStar(require("../utils/character_utils")); const CachedAssetsManager_1 = require("./CachedAssetsManager"); const CharacterData_1 = require("../models/character/CharacterData"); const WeaponData_1 = require("../models/weapon/WeaponData"); const Costume_1 = require("../models/character/Costume"); const Material_1 = require("../models/material/Material"); const ArtifactData_1 = require("../models/artifact/ArtifactData"); const DetailedGenshinUser_1 = require("../models/DetailedGenshinUser"); const enka_system_1 = require("enka-system"); const GenshinCharacterBuild_1 = require("../models/enka/GenshinCharacterBuild"); const Material_2 = require("../models/material/Material"); const ArtifactSet_1 = require("../models/artifact/ArtifactSet"); const config_file_js_1 = require("config_file.js"); const ExcelTransformer_1 = require("./ExcelTransformer"); const fetch_utils_1 = require("../utils/fetch_utils"); const FetchFailedError_1 = require("../errors/FetchFailedError"); const getUserUrl = (enkaUrl, uid) => `${enkaUrl}/api/uid/${uid}`; const userCacheMap = new Map(); exports.defaultImageBaseUrls = [ { url: "https://homdgcat.wiki/homdgcat-res", priority: 12, format: "PNG", regexList: [ /^UI_(RelicIcon|EquipIcon|ItemIcon)_/, /^UI_AvatarIcon_(?!Side_)(.+)(?<!_(Card|Circle))$/, /^Skill_/, /^UI_Gacha_AvatarImg_/, /^UI_NameCardPic_(.+)_P$/, ], customParser(fileName) { if (fileName.startsWith("UI_AvatarIcon_")) { return `Avatar/${fileName}.png`; } else if (fileName.startsWith("UI_RelicIcon_")) { return `Relic/${fileName}.png`; } else if (fileName.startsWith("UI_EquipIcon_")) { return `Weapon/${fileName}.png`; } else if (fileName.startsWith("UI_ItemIcon_")) { return `Mat/${fileName}.png`; } else if (fileName.startsWith("Skill_")) { return `AvatarSkill/${fileName}.png`; } else if (fileName.startsWith("UI_Gacha_AvatarImg_")) { return `Gacha/${fileName}.png`; } else if (fileName.startsWith("UI_NameCardPic_") && fileName.endsWith("_P")) { return `Avatar/${fileName}.png`; } else { throw new Error(`Unhandled file name: ${fileName}`); } }, }, { url: "https://enka.network/ui", priority: 10, format: "PNG", regexList: [ /^UI_(Costume|NameCardIcon|NameCardPic|RelicIcon|AvatarIcon_Side|EquipIcon)_/, /^UI_AvatarIcon_(.+)_(Card|Circle)$/, ], }, { url: "https://gi.yatta.moe/assets/UI", priority: 3, format: "PNG", regexList: [ /.*/, ], }, { url: "https://api.hakush.in/gi/UI", priority: 2, format: "WEBP", regexList: [ /.*/, ], }, ]; exports.defaultEnkaClientOptions = { "enkaUrl": "https://enka.network", "imageBaseUrls": [...exports.defaultImageBaseUrls], "userAgent": "Mozilla/5.0", "requestTimeout": 3000, "defaultLanguage": "en", "textAssetsDynamicData": { paramList: [], userInfo: null, }, "cacheDirectory": null, "showFetchCacheLog": true, "userCache": { isEnabled: true, getter: null, setter: null, deleter: null, }, "gameDataBaseUrl": "https://gitlab.com/Dimbreath/AnimeGameData/-/raw/master", "githubToken": null, "enkaSystem": null, }; class EnkaClient { getUser(data) { const fixedData = (0, config_file_js_1.renameKeys)(data, { "player_info": "playerInfo" }); return new GenshinUser_1.GenshinUser(fixedData, this); } getCharacterBuild(data, username, hash) { return new GenshinCharacterBuild_1.GenshinCharacterBuild(data, this, username, hash); } /** @param options Options for the client */ constructor(options = {}) { this._tasks = []; this.hoyoType = 0; const mergedOptions = (0, config_file_js_1.bindOptions)(exports.defaultEnkaClientOptions, options); if (!mergedOptions.enkaSystem) { if (enka_system_1.EnkaSystem.instance.getLibrary(this.hoyoType)) { mergedOptions.enkaSystem = new enka_system_1.EnkaSystem(); } else { mergedOptions.enkaSystem = enka_system_1.EnkaSystem.instance; } } this.options = mergedOptions; const userCacheFuncs = [this.options.userCache.getter, this.options.userCache.setter, this.options.userCache.deleter]; if (userCacheFuncs.some(f => f) && userCacheFuncs.some(f => !f)) throw new Error("All user cache functions (setter/getter/deleter) must be null or all must be customized."); this.cachedAssetsManager = new CachedAssetsManager_1.CachedAssetsManager(this); this.options.enkaSystem.registerLibrary(this); } /** * @param uid In-game UID of the user * @param collapse Whether to fetch rough user information (Very fast) * @returns DetailedGenshinUser if collapse is false, GenshinUser if collapse is true * @throws {EnkaNetworkError} */ async _fetchUser(uid, collapse) { var _a, _b, _c, _d; if (isNaN(Number(uid))) throw new Error("Parameter `uid` must be a number or a string number."); const cacheGetter = (_a = this.options.userCache.getter) !== null && _a !== void 0 ? _a : (async (key) => userCacheMap.get(key)); const cacheSetter = (_b = this.options.userCache.setter) !== null && _b !== void 0 ? _b : (async (key, data) => { userCacheMap.set(key, data); }); const cacheDeleter = (_c = this.options.userCache.deleter) !== null && _c !== void 0 ? _c : (async (key) => { userCacheMap.delete(key); }); const cacheKey = `${uid}${collapse ? "-info" : ""}`; const cachedUserData = (_d = (collapse ? await cacheGetter(cacheKey) : null)) !== null && _d !== void 0 ? _d : await cacheGetter(uid.toString()); const useCache = !!(cachedUserData && this.options.userCache.isEnabled); let data; if (!useCache) { const url = getUserUrl(this.options.enkaUrl, uid) + (collapse ? "?info" : ""); let response; try { response = await (0, fetch_utils_1.fetchString)({ url, enka: this, enableTimeout: true }); } catch (e) { const fetchFailedError = e instanceof FetchFailedError_1.FetchFailedError ? e : null; // TODO: better error instead of making statusCode -1 if (!fetchFailedError) throw new enka_system_1.EnkaNetworkError(`Request to enka.network failed with unknown error: ${e}`, -1, "Unknown Error"); switch (fetchFailedError.status) { case 400: throw new enka_system_1.InvalidUidFormatError(Number(uid), fetchFailedError.status, fetchFailedError.statusText); case 424: throw new enka_system_1.EnkaNetworkError("Request to enka.network failed because it is under maintenance.", fetchFailedError.status, fetchFailedError.statusText); case 429: throw new enka_system_1.EnkaNetworkError("Rate Limit reached. You reached enka.network's rate limit. Please try again in a few minutes.", fetchFailedError.status, fetchFailedError.statusText); case 404: throw new enka_system_1.UserNotFoundError(`User with uid ${uid} was not found. Please check whether the uid is correct. If you find the uid is correct, it may be a internal server error.`, fetchFailedError.status, fetchFailedError.statusText); default: throw new enka_system_1.EnkaNetworkError(`Request to enka.network failed with unknown status code ${fetchFailedError.status} - ${fetchFailedError.statusText}\nRequest url: ${url}`, fetchFailedError.status, fetchFailedError.statusText); } } data = JSON.parse(response); if (this.options.userCache.isEnabled) { const lifetime = data.ttl * 1000; const now = Date.now(); data._lib = { cache_id: (0, config_file_js_1.generateUuid)(), created_at: now, expires_at: now + lifetime, original_ttl: data.ttl }; const task = setTimeout(async () => { const dataToDelete = await cacheGetter(cacheKey); if (!dataToDelete) return; if (dataToDelete._lib.cache_id === data._lib.cache_id) { await cacheDeleter(cacheKey); } this._tasks.splice(this._tasks.indexOf(task), 1); }, lifetime); this._tasks.push(task); if (!collapse) await cacheDeleter(`${uid}-info`); await cacheSetter(cacheKey, data); } } else { // TODO: use structuredClone data = Object.assign({}, cachedUserData); if (collapse) delete data["avatarInfoList"]; data.ttl = Math.ceil((data._lib.expires_at - Date.now()) / 1000); } // console.log("useCache", useCache); const userData = (0, config_file_js_1.bindOptions)(data, { _lib: { is_cache: useCache } }); const user = collapse ? new GenshinUser_1.GenshinUser(userData, this) : new DetailedGenshinUser_1.DetailedGenshinUser(userData, this); return user; } /** * @param uid In-game UID of the user */ async fetchUser(uid) { return await this._fetchUser(uid, false); } /** * @param uid In-game UID of the user */ async fetchCollapsedUser(uid) { return await this._fetchUser(uid, true); } /** * @param username enka.network username, not in-game nickname * @returns the genshin accounts added to the Enka.Network account */ async fetchEnkaGenshinAccounts(username) { return await this.options.enkaSystem.fetchEnkaGameAccounts(username, [0]); } /** * @param username enka.network username, not in-game nickname * @param hash EnkaGameAccount hash * @returns the genshin account with provided hash */ async fetchEnkaGenshinAccount(username, hash) { return await this.options.enkaSystem.fetchEnkaGameAccount(username, hash); } /** * @param username enka.network username, not in-game nickname * @param hash EnkaGameAccount hash * @returns the genshin character builds including saved builds in Enka.Network account */ async fetchEnkaGenshinBuilds(username, hash) { return await this.options.enkaSystem.fetchEnkaCharacterBuilds(username, hash); } /** * @returns all playable character data */ getAllCharacters() { return Object.values(this.cachedAssetsManager.getExcelData("AvatarExcelConfigData")) .map(c => new config_file_js_1.JsonReader(ExcelTransformer_1.excelJsonOptions, c)) .filter(j => j.getAsStringWithDefault(null, "useType") === "AVATAR_FORMAL" && j.getAsNumber("featureTagGroupID") === j.getAsNumber("id")) .map(j => characterUtils.getCharactersById(j.getAsNumber("id"), this)).reduce((a, b) => [...a, ...b], []); } /** * @param id The id of the character * @param skillDepotId Specifies one or zero elements for Traveler */ getCharacterById(id, skillDepotId) { if (isNaN(Number(id))) throw new Error("Parameter `id` must be a number or a string number."); return CharacterData_1.CharacterData.getById(Number(id), this, skillDepotId ? Number(skillDepotId) : undefined); } /** * @returns all weapon data */ getAllWeapons(excludeInvalidWeapons = true, filterByCodex = true) { let weapons = Object.values(this.cachedAssetsManager.getExcelData("WeaponExcelConfigData")) .map(w => new config_file_js_1.JsonReader(ExcelTransformer_1.excelJsonOptions, w)); if (excludeInvalidWeapons) { weapons = weapons.filter(j => j.has("id") && j.has("weaponPromoteId") && j.getAsNumber("weaponPromoteId") === j.getAsNumber("id")); } if (filterByCodex) { const codexSet = new Set(Object.keys(this.cachedAssetsManager.getExcelData("WeaponCodexExcelConfigData"))); weapons = weapons.filter(j => j.has("id") && codexSet.has(j.getAsNumber("id").toString())); } return weapons.map(j => new WeaponData_1.WeaponData(j.getAsJsonObject(), this)); } /** * @param id The id of the weapon */ getWeaponById(id) { if (isNaN(Number(id))) throw new Error("Parameter `id` must be a number or a string number."); return WeaponData_1.WeaponData.getById(Number(id), this); } /** * @param includeDefaults Whether to include default costumes * @returns all costume data */ getAllCostumes(includeDefaults = false) { return Object.values(this.cachedAssetsManager.getExcelData("AvatarCostumeExcelConfigData")) .flatMap(c => Object.values(c)) .filter(c => !includeDefaults || (includeDefaults && new config_file_js_1.JsonReader(ExcelTransformer_1.excelJsonOptions, c).getAsBooleanWithDefault(false, "isDefault"))) .map(c => new Costume_1.Costume(c, this)); } /** * @param id The id of the costume */ getCostumeById(id) { if (isNaN(Number(id))) throw new Error("Parameter `id` must be a number or a string number."); return Costume_1.Costume.getBySkinId(Number(id), this); } /** * @returns all material data */ getAllMaterials() { return Object.values(this.cachedAssetsManager.getExcelData("MaterialExcelConfigData")) .map(m => Material_2.Material.getMaterialByData(m, this)); } /** * @param id The id of the material */ getMaterialById(id) { if (isNaN(Number(id))) throw new Error("Parameter `id` must be a number or a string number."); return Material_2.Material.getMaterialById(Number(id), this); } /** * @returns all name card data */ getAllNameCards() { return Object.values(this.cachedAssetsManager.getExcelData("MaterialExcelConfigData")) .map(m => new config_file_js_1.JsonReader(ExcelTransformer_1.excelJsonOptions, m)) .filter(j => j.has("materialType") && j.getAsString("materialType") === Material_1.NameCard.MATERIAL_TYPE) .map(j => new Material_1.NameCard(j.getAsJsonObject(), this)); } /** * @param id The id of the name card */ getNameCardById(id) { if (isNaN(Number(id))) throw new Error("Parameter `id` must be a number or a string number."); const material = Material_2.Material.getMaterialById(Number(id), this); if (material.materialType !== Material_1.NameCard.MATERIAL_TYPE) throw new Error(`Material ${material.id} is not a NameCard.`); return material; } /** * @param highestRarityOnly Whether to return only the rarest of artifacts of the same type * @returns all artifact data */ getAllArtifacts(highestRarityOnly = false) { const allArtifacts = this.cachedAssetsManager.getExcelData("ReliquaryExcelConfigData"); const artifacts = []; for (const setCodex of Object.values(this.cachedAssetsManager.getExcelData("ReliquaryCodexExcelConfigData"))) { const rarities = highestRarityOnly ? [Math.max(...Object.keys(setCodex).map(k => Number(k)))] : Object.keys(setCodex); for (const rarity of rarities) { const codex = new config_file_js_1.JsonReader(ExcelTransformer_1.excelJsonOptions, setCodex[rarity]); const ids = [ codex.getAsNumberWithDefault(0, "cupId"), codex.getAsNumberWithDefault(0, "leatherId"), codex.getAsNumberWithDefault(0, "capId"), codex.getAsNumberWithDefault(0, "flowerId"), codex.getAsNumberWithDefault(0, "sandId"), ].filter(n => n !== 0); artifacts.push(...ids.map(id => new ArtifactData_1.ArtifactData(allArtifacts[id], this))); } } ; return artifacts; } /** * @param id The id of the artifact */ getArtifactById(id) { if (isNaN(Number(id))) throw new Error("Parameter `id` must be a number or a string number."); return ArtifactData_1.ArtifactData.getById(Number(id), this); } /** * @returns all artifact set data */ getAllArtifactSets() { return Object.values(this.cachedAssetsManager.getExcelData("ReliquarySetExcelConfigData")) .filter(s => s["disableFilter"] !== 1) .map(s => new ArtifactSet_1.ArtifactSet(s, this)); } /** * @param id The id of artifact set */ getArtifactSetById(id) { if (isNaN(Number(id))) throw new Error("Parameter `id` must be a number or a string number."); return ArtifactSet_1.ArtifactSet.getById(Number(id), this); } /** * Clear all running tasks in the client. */ close() { this._tasks.forEach(task => clearTimeout(task)); } } exports.EnkaClient = EnkaClient;