enka.ts 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +206 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# enka.ts
|
|
2
|
+
|
|
3
|
+
Typed Genshin Impact enka.network API wrapper that works
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Built-in TTL cache using Enka's response `ttl`
|
|
8
|
+
- Client-side rate limiter (default: 50 requests / minute)
|
|
9
|
+
- Automatic avatar resolution using https://gi.yatta.moe/api/v2/en/avatar
|
|
10
|
+
- Fully typed responses
|
|
11
|
+
- Typed error handling
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
$ bun i enka.ts
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { ApiError, fetchEnka } from "enka.ts";
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const player = await fetchEnka("800123456");
|
|
26
|
+
|
|
27
|
+
// Or use ?info endpoint to exclude build details for faster query
|
|
28
|
+
const playerInfo = await fetchEnka("800123456", "info");
|
|
29
|
+
|
|
30
|
+
console.log(player.playerInfo.nickname);
|
|
31
|
+
} catch(err) {
|
|
32
|
+
if(err instanceof ApiError) {
|
|
33
|
+
if(err.type === "uid_format") // handle invalid uid format
|
|
34
|
+
else if(err.type === "not_found") // handle user not found
|
|
35
|
+
else if(err.type === "maintenance") // handle maintenance
|
|
36
|
+
else if(err.type === "internal") // handle internal server error
|
|
37
|
+
else if(err.type === "ratelimit") // handle rate limit exceeded
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> [!NOTE]
|
|
43
|
+
> Locations that normally contain `avatarId` will also include a resolved
|
|
44
|
+
> `avatar` object:
|
|
45
|
+
> ```ts
|
|
46
|
+
> {
|
|
47
|
+
> avatarId: number;
|
|
48
|
+
> name: string;
|
|
49
|
+
> element: AvatarElement;
|
|
50
|
+
> }
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
declare class TTLCache {
|
|
2
|
+
private cache;
|
|
3
|
+
get<T>(key: string): T | undefined;
|
|
4
|
+
clean(): void;
|
|
5
|
+
set<T>(key: string, value: T, ttlSeconds: number): void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
declare namespace ApiResponse {
|
|
9
|
+
interface Root {
|
|
10
|
+
uid: string;
|
|
11
|
+
ttl: number;
|
|
12
|
+
playerInfo: PlayerInfo;
|
|
13
|
+
avatarInfoList?: AvatarInfo[];
|
|
14
|
+
}
|
|
15
|
+
interface PlayerInfo {
|
|
16
|
+
nickname: string;
|
|
17
|
+
signature: string;
|
|
18
|
+
level: number;
|
|
19
|
+
worldLevel: number;
|
|
20
|
+
namecardId: number;
|
|
21
|
+
finishAchievementNum: number;
|
|
22
|
+
towerFloorIndex: number;
|
|
23
|
+
towerLevelIndex: number;
|
|
24
|
+
showAvatarInfoList: ShowAvatarInfo[];
|
|
25
|
+
showNameCardIdList: number[];
|
|
26
|
+
profilePicture: {
|
|
27
|
+
avatarId?: number;
|
|
28
|
+
id?: number;
|
|
29
|
+
costumeId?: number;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
interface ShowAvatarInfo {
|
|
33
|
+
avatarId: number;
|
|
34
|
+
level: number;
|
|
35
|
+
costumeId?: number;
|
|
36
|
+
}
|
|
37
|
+
interface AvatarInfo {
|
|
38
|
+
avatarId: number;
|
|
39
|
+
propMap: Record<string, PropValue>;
|
|
40
|
+
fightPropMap: Record<string, number>;
|
|
41
|
+
skillDepotId: number;
|
|
42
|
+
inherentProudSkillList?: number[];
|
|
43
|
+
talentIdList?: number[];
|
|
44
|
+
skillLevelMap?: Record<string, number>;
|
|
45
|
+
equipList?: Equip[];
|
|
46
|
+
fetterInfo?: {
|
|
47
|
+
expLevel: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
interface PropValue {
|
|
51
|
+
type: number;
|
|
52
|
+
ival?: string;
|
|
53
|
+
val?: number;
|
|
54
|
+
}
|
|
55
|
+
interface Equip {
|
|
56
|
+
itemId: number;
|
|
57
|
+
reliquary?: ArtifactData;
|
|
58
|
+
weapon?: WeaponData;
|
|
59
|
+
flat: FlatData;
|
|
60
|
+
}
|
|
61
|
+
interface WeaponData {
|
|
62
|
+
level: number;
|
|
63
|
+
promoteLevel: number;
|
|
64
|
+
affixMap?: Record<string, number>;
|
|
65
|
+
}
|
|
66
|
+
interface ArtifactData {
|
|
67
|
+
level: number;
|
|
68
|
+
mainPropId: number;
|
|
69
|
+
appendPropIdList?: number[];
|
|
70
|
+
}
|
|
71
|
+
interface FlatData {
|
|
72
|
+
nameTextMapHash: number;
|
|
73
|
+
setNameTextMapHash?: number;
|
|
74
|
+
icon: string;
|
|
75
|
+
rankLevel?: number;
|
|
76
|
+
reliquaryMainstat?: {
|
|
77
|
+
mainPropId: string;
|
|
78
|
+
statValue: number;
|
|
79
|
+
};
|
|
80
|
+
reliquarySubstats?: {
|
|
81
|
+
appendPropId: string;
|
|
82
|
+
statValue: number;
|
|
83
|
+
}[];
|
|
84
|
+
weaponStats?: {
|
|
85
|
+
appendPropId: string;
|
|
86
|
+
statValue: number;
|
|
87
|
+
}[];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
declare enum AvatarElement {
|
|
91
|
+
Pyro = "Fire",
|
|
92
|
+
Hydro = "Water",
|
|
93
|
+
Electro = "Electric",
|
|
94
|
+
Cryo = "Ice",
|
|
95
|
+
Dendro = "Grass",
|
|
96
|
+
Anemo = "Wind",
|
|
97
|
+
Geo = "Rock"
|
|
98
|
+
}
|
|
99
|
+
interface BaseAvatar {
|
|
100
|
+
avatarId: number;
|
|
101
|
+
name: string;
|
|
102
|
+
element: AvatarElement;
|
|
103
|
+
}
|
|
104
|
+
type ResolveAvatar<T extends {
|
|
105
|
+
avatarId: number;
|
|
106
|
+
}> = Omit<T, "avatarId"> & {
|
|
107
|
+
avatar: BaseAvatar;
|
|
108
|
+
};
|
|
109
|
+
type ResolvedProfilePicture = Omit<ApiResponse.PlayerInfo["profilePicture"], "avatarId"> & {
|
|
110
|
+
avatar?: BaseAvatar;
|
|
111
|
+
};
|
|
112
|
+
type ResolvedPlayerInfo = Omit<ApiResponse.PlayerInfo, "showAvatarInfoList" | "profilePicture"> & {
|
|
113
|
+
profilePicture: ResolvedProfilePicture;
|
|
114
|
+
showAvatarInfoList: ResolveAvatar<ApiResponse.ShowAvatarInfo>[];
|
|
115
|
+
};
|
|
116
|
+
type ResolvedAvatarInfo = ResolveAvatar<ApiResponse.AvatarInfo>;
|
|
117
|
+
type ResolvedRoot = Omit<ApiResponse.Root, "playerInfo" | "avatarInfoList"> & {
|
|
118
|
+
playerInfo: ResolvedPlayerInfo;
|
|
119
|
+
avatarInfoList?: ResolvedAvatarInfo[];
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
declare const uidRegex: RegExp;
|
|
123
|
+
type Mode = "full" | "info";
|
|
124
|
+
declare function fetchEnka(uid: string, mode?: Mode): Promise<ResolvedRoot>;
|
|
125
|
+
|
|
126
|
+
type ApiErrorType = "uid_format" | "not_found" | "maintenance" | "internal" | "ratelimit";
|
|
127
|
+
declare class ApiError extends Error {
|
|
128
|
+
type: ApiErrorType;
|
|
129
|
+
constructor(type: ApiErrorType, message?: string);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
declare class RateLimiter {
|
|
133
|
+
private limit;
|
|
134
|
+
private mode;
|
|
135
|
+
private active;
|
|
136
|
+
private queue;
|
|
137
|
+
constructor(limit: number, interval: number, mode?: "wait" | "throw");
|
|
138
|
+
private flush;
|
|
139
|
+
fill(): void;
|
|
140
|
+
acquire(): Promise<void>;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
declare function resolveRoot(data: ApiResponse.Root): Promise<ResolvedRoot>;
|
|
144
|
+
|
|
145
|
+
export { ApiError, ApiResponse, AvatarElement, type BaseAvatar, type Mode, RateLimiter, type ResolveAvatar, type ResolvedAvatarInfo, type ResolvedPlayerInfo, type ResolvedProfilePicture, type ResolvedRoot, TTLCache, fetchEnka, resolveRoot, uidRegex };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// src/cache.ts
|
|
2
|
+
var TTLCache = class {
|
|
3
|
+
// biome-ignore lint/suspicious/noExplicitAny: cache mapping
|
|
4
|
+
cache = /* @__PURE__ */ new Map();
|
|
5
|
+
get(key) {
|
|
6
|
+
const entry = this.cache.get(key);
|
|
7
|
+
if (!entry) return;
|
|
8
|
+
if (Date.now() > entry.expires) {
|
|
9
|
+
this.cache.delete(key);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
queueMicrotask(this.clean.bind(this));
|
|
13
|
+
return entry.value;
|
|
14
|
+
}
|
|
15
|
+
clean() {
|
|
16
|
+
for (const [key, entry] of this.cache) {
|
|
17
|
+
if (Date.now() > entry.expires) {
|
|
18
|
+
this.cache.delete(key);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
set(key, value, ttlSeconds) {
|
|
24
|
+
this.cache.set(key, {
|
|
25
|
+
value,
|
|
26
|
+
expires: Date.now() + ttlSeconds * 1e3
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/errors.ts
|
|
32
|
+
var ApiError = class extends Error {
|
|
33
|
+
type;
|
|
34
|
+
constructor(type, message) {
|
|
35
|
+
if (message) super(message);
|
|
36
|
+
else
|
|
37
|
+
switch (type) {
|
|
38
|
+
case "uid_format":
|
|
39
|
+
super("Invalid UID format");
|
|
40
|
+
break;
|
|
41
|
+
case "not_found":
|
|
42
|
+
super("User not found");
|
|
43
|
+
break;
|
|
44
|
+
case "maintenance":
|
|
45
|
+
super("Enka is under maintenance");
|
|
46
|
+
break;
|
|
47
|
+
case "internal":
|
|
48
|
+
super("Internal server error");
|
|
49
|
+
break;
|
|
50
|
+
case "ratelimit":
|
|
51
|
+
super("Rate limit exceeded");
|
|
52
|
+
break;
|
|
53
|
+
default:
|
|
54
|
+
super("Unknown Error");
|
|
55
|
+
}
|
|
56
|
+
this.type = type;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/ratelimit.ts
|
|
61
|
+
var RateLimiter = class {
|
|
62
|
+
constructor(limit, interval, mode = "wait") {
|
|
63
|
+
this.limit = limit;
|
|
64
|
+
this.mode = mode;
|
|
65
|
+
setInterval(() => {
|
|
66
|
+
this.active = 0;
|
|
67
|
+
this.flush();
|
|
68
|
+
}, interval);
|
|
69
|
+
}
|
|
70
|
+
active = 0;
|
|
71
|
+
queue = [];
|
|
72
|
+
flush() {
|
|
73
|
+
while (this.queue.length && this.active < this.limit) {
|
|
74
|
+
this.active++;
|
|
75
|
+
const next = this.queue.shift();
|
|
76
|
+
next();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
fill() {
|
|
80
|
+
this.active = this.limit;
|
|
81
|
+
}
|
|
82
|
+
async acquire() {
|
|
83
|
+
if (this.active < this.limit) {
|
|
84
|
+
this.active++;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (this.mode === "throw") {
|
|
88
|
+
throw new ApiError("ratelimit", "Rate limit exceeded (Package side)");
|
|
89
|
+
}
|
|
90
|
+
await new Promise((resolve) => {
|
|
91
|
+
this.queue.push(resolve);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/types.ts
|
|
97
|
+
var AvatarElement = /* @__PURE__ */ ((AvatarElement3) => {
|
|
98
|
+
AvatarElement3["Pyro"] = "Fire";
|
|
99
|
+
AvatarElement3["Hydro"] = "Water";
|
|
100
|
+
AvatarElement3["Electro"] = "Electric";
|
|
101
|
+
AvatarElement3["Cryo"] = "Ice";
|
|
102
|
+
AvatarElement3["Dendro"] = "Grass";
|
|
103
|
+
AvatarElement3["Anemo"] = "Wind";
|
|
104
|
+
AvatarElement3["Geo"] = "Rock";
|
|
105
|
+
return AvatarElement3;
|
|
106
|
+
})(AvatarElement || {});
|
|
107
|
+
|
|
108
|
+
// src/assets.ts
|
|
109
|
+
var avatarMap = null;
|
|
110
|
+
async function loadAvatars() {
|
|
111
|
+
if (avatarMap) return avatarMap;
|
|
112
|
+
const res = await fetch("https://gi.yatta.moe/api/v2/en/avatar");
|
|
113
|
+
const { data } = await res.json();
|
|
114
|
+
avatarMap = new Map(
|
|
115
|
+
Object.values(data.items).map((c) => [
|
|
116
|
+
c.id,
|
|
117
|
+
{
|
|
118
|
+
avatarId: c.id,
|
|
119
|
+
name: c.name,
|
|
120
|
+
element: c.element
|
|
121
|
+
}
|
|
122
|
+
])
|
|
123
|
+
);
|
|
124
|
+
return avatarMap;
|
|
125
|
+
}
|
|
126
|
+
async function resolveAvatarId(id) {
|
|
127
|
+
const map = await loadAvatars();
|
|
128
|
+
const avatar = map.get(id);
|
|
129
|
+
if (!avatar) throw new Error(`Unknown avatar ${id}`);
|
|
130
|
+
return avatar;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/resolve.ts
|
|
134
|
+
async function resolveRoot(data) {
|
|
135
|
+
const playerInfo = {
|
|
136
|
+
...data.playerInfo,
|
|
137
|
+
profilePicture: {
|
|
138
|
+
...data.playerInfo.profilePicture,
|
|
139
|
+
avatar: data.playerInfo.profilePicture.avatarId ? await resolveAvatarId(data.playerInfo.profilePicture.avatarId) : void 0
|
|
140
|
+
},
|
|
141
|
+
showAvatarInfoList: await Promise.all(
|
|
142
|
+
data.playerInfo.showAvatarInfoList.map(async (a) => ({
|
|
143
|
+
...a,
|
|
144
|
+
avatar: await resolveAvatarId(a.avatarId)
|
|
145
|
+
}))
|
|
146
|
+
)
|
|
147
|
+
};
|
|
148
|
+
let avatarInfoList;
|
|
149
|
+
if (data.avatarInfoList) {
|
|
150
|
+
avatarInfoList = await Promise.all(
|
|
151
|
+
data.avatarInfoList.map(async (a) => ({
|
|
152
|
+
...a,
|
|
153
|
+
avatar: await resolveAvatarId(a.avatarId)
|
|
154
|
+
}))
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
...data,
|
|
159
|
+
playerInfo,
|
|
160
|
+
avatarInfoList
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/enka.ts
|
|
165
|
+
var uidRegex = /^(?:[0-35-9]|18)[0-9]{8}$/;
|
|
166
|
+
var cache = new TTLCache();
|
|
167
|
+
var limiter = new RateLimiter(50, 6e4, "wait");
|
|
168
|
+
async function fetchEnka(uid, mode = "full") {
|
|
169
|
+
if (!uidRegex.test(uid)) throw new ApiError("uid_format");
|
|
170
|
+
const key = `enka:${uid}:${mode}`;
|
|
171
|
+
const cached = cache.get(key);
|
|
172
|
+
if (cached) return cached;
|
|
173
|
+
await limiter.acquire();
|
|
174
|
+
const url = mode === "info" ? `https://enka.network/api/uid/${uid}?info` : `https://enka.network/api/uid/${uid}`;
|
|
175
|
+
const res = await fetch(url);
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
switch (res.status) {
|
|
178
|
+
case 400:
|
|
179
|
+
throw new ApiError("uid_format");
|
|
180
|
+
case 404:
|
|
181
|
+
throw new ApiError("not_found");
|
|
182
|
+
case 424:
|
|
183
|
+
throw new ApiError("maintenance");
|
|
184
|
+
case 429:
|
|
185
|
+
throw new ApiError("ratelimit", "Rate limit exceeded (server-side)");
|
|
186
|
+
case 500:
|
|
187
|
+
case 503:
|
|
188
|
+
throw new ApiError("internal");
|
|
189
|
+
default:
|
|
190
|
+
throw new Error(`Enka request failed (${res.status})`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const data = await res.json();
|
|
194
|
+
const resolved = await resolveRoot(data);
|
|
195
|
+
cache.set(key, resolved, data.ttl);
|
|
196
|
+
return resolved;
|
|
197
|
+
}
|
|
198
|
+
export {
|
|
199
|
+
ApiError,
|
|
200
|
+
AvatarElement,
|
|
201
|
+
RateLimiter,
|
|
202
|
+
TTLCache,
|
|
203
|
+
fetchEnka,
|
|
204
|
+
resolveRoot,
|
|
205
|
+
uidRegex
|
|
206
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "enka.ts",
|
|
3
|
+
"description": "Typed Genshin Impact enka.network API wrapper that works",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@biomejs/biome": "^2.4.6",
|
|
8
|
+
"@types/bun": "latest",
|
|
9
|
+
"tsup": "^8.5.1"
|
|
10
|
+
},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"typescript": "^5"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
24
|
+
"test": "bun test"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"genshin",
|
|
28
|
+
"enka",
|
|
29
|
+
"genshin-impact",
|
|
30
|
+
"enka-api"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT"
|
|
33
|
+
}
|