enka-network-api
Version:
Enka-network API wrapper for Genshin Impact.
429 lines (428 loc) • 19.4 kB
JavaScript
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;
;