1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
|
/**
* Fetch the raw Hypixel API
*/
import fetch from 'node-fetch'
import * as nodeFetch from 'node-fetch'
import { jsonToQuery, shuffle } from './util.js'
import { Agent } from 'https'
if (!process.env.hypixel_keys)
// if there's no hypixel keys in env, run dotenv
(await import('dotenv')).config()
// We need to create an agent to prevent memory leaks and to only do dns lookups once
const httpsAgent = new Agent({
keepAlive: true
})
/** This array should only ever contain one item because using multiple hypixel api keys isn't allowed :) */
const apiKeys = process.env?.hypixel_keys?.split(' ') ?? []
interface KeyUsage {
remaining: number
limit: number
reset: number
}
const apiKeyUsage: { [ key: string ]: KeyUsage } = {}
// the usage amount the api key was on right before it reset
const apiKeyMaxUsage: { [ key: string ]: number } = {}
const baseHypixelAPI = 'https://api.hypixel.net'
/** Choose the best current API key */
export function chooseApiKey(): string | null {
// find the api key with the lowest amount of uses
let bestKeyUsage: KeyUsage | null = null
let bestKey: string | null = null
// we limit to 5 api keys because otherwise they get automatically banned
for (let key of shuffle(apiKeys.slice(0, 5))) {
const keyUsage = apiKeyUsage[key]
// if the key has never been used before, use it
if (!keyUsage) return key
// if the key has reset since the last use, set the remaining count to the default
if (Date.now() > keyUsage.reset) {
apiKeyMaxUsage[key] = keyUsage.limit - keyUsage.remaining
keyUsage.remaining = keyUsage.limit
}
// if this key has more uses remaining than the current known best one, save it
if (bestKeyUsage === null || keyUsage.remaining > bestKeyUsage.remaining) {
bestKeyUsage = keyUsage
bestKey = key
}
}
return bestKey
}
export function getKeyUsage() {
let keyLimit = 0
let keyUsage = 0
for (let key of Object.values(apiKeyMaxUsage)) {
// if the key isn't in apiKeyUsage, continue
if (!apiKeyUsage[key]) continue
keyLimit += apiKeyUsage[key].limit
keyUsage += apiKeyMaxUsage[key]
}
return {
limit: keyLimit,
usage: keyUsage
}
}
export interface HypixelResponse {
[key: string]: any | {
success: boolean
throttled?: boolean
}
}
export interface HypixelPlayerStatsSkyBlockProfiles {
[ uuid: string ]: {
profile_id: string
cute_name: string
}
}
interface HypixelPlayerStatsSkyBlock {
profiles: HypixelPlayerStatsSkyBlockProfiles
}
export interface HypixelPlayerSocialMedia {
YOUTUBE?: string
prompt: boolean
links: {
DISCORD?: string
HYPIXEL?: string
}
}
export interface HypixelPlayer {
_id: string
achievementsOneTime: string[]
displayname: string
firstLogin: number,
lastLogin: number,
lastLogout: number
knownAliases: string[],
knownAliasesLower: string[]
networkExp: number
playername: string
stats: {
SkyBlock: HypixelPlayerStatsSkyBlock
[ name: string ]: any
},
timePlaying: number,
uuid: string,
achievements: { [ name: string ]: number },
petConsumables: { [ name: string ]: number },
vanityMeta: {
packages: string[]
},
language: string,
userLanguage?: string
packageRank?: string
newPackageRank?: string
rankPlusColor?: string
monthlyPackageRank?: string
rank?: string
prefix?: string
claimed_potato_talisman?: number
skyblock_free_cookie?: number
socialMedia?: HypixelPlayerSocialMedia
}
/** Send an HTTP request to the Hypixel API */
export let sendApiRequest = async function sendApiRequest({ path, key, args }): Promise<HypixelResponse> {
// Send a raw http request to api.hypixel.net, and return the parsed json
if (key)
// If there's an api key, add it to the arguments
args.key = key
// Construct a url from the base api url, path, and arguments
const fetchUrl = baseHypixelAPI + '/' + path + '?' + jsonToQuery(args)
let fetchResponse: nodeFetch.Response
let fetchJsonParsed: any
try {
fetchResponse = await fetch(
fetchUrl,
{ agent: () => httpsAgent }
)
fetchJsonParsed = await fetchResponse.json()
} catch {
// if there's an error, wait a second and try again
await new Promise((resolve) => setTimeout(resolve, 1000))
return await sendApiRequest({ path, key, args })
}
// bruh
if (fetchJsonParsed.cause === 'This endpoint is currently disabled') {
await new Promise((resolve) => setTimeout(resolve, 30000))
return await sendApiRequest({ path, key, args })
}
// if the cause is "Invalid API key", remove the key from the list of keys and try again
if (fetchJsonParsed.cause === 'Invalid API key') {
if (apiKeys.includes(key)) {
apiKeys.splice(apiKeys.indexOf(key), 1)
console.log(`${key} is invalid, removing it from the list of keys`)
}
return await sendApiRequest({ path, key: chooseApiKey(), args })
}
if (fetchResponse.headers.get('ratelimit-limit'))
// remember how many uses it has
apiKeyUsage[key] = {
remaining: parseInt(fetchResponse.headers.get('ratelimit-remaining') ?? '0'),
limit: parseInt(fetchResponse.headers.get('ratelimit-limit') ?? '0'),
reset: Date.now() + parseInt(fetchResponse.headers.get('ratelimit-reset') ?? '0') * 1000,
}
if (fetchJsonParsed.throttle) {
if (apiKeyUsage[key])
apiKeyUsage[key].remaining = 0
// if it's throttled, wait 10 seconds and try again
await new Promise((resolve) => setTimeout(resolve, 10000))
return await sendApiRequest({ path, key, args })
}
return fetchJsonParsed
}
// this is necessary for mocking in the tests because es6
export function mockSendApiRequest($value) { sendApiRequest = $value }
|