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
|
/**
* Fetch the raw Hypixel API
*/
import { shuffle, sleep } from './util.js'
import typedHypixelApi from 'typed-hypixel-api'
import { Agent } from 'https'
if (!process.env.hypixel_keys)
// if there's no hypixel keys in env, run dotenv
(await import('dotenv')).config()
/** 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 } = {}
/** 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.keys(apiKeyMaxUsage)) {
// if the key isn't in apiKeyUsage, continue
if (!apiKeyUsage[key]) continue
keyUsage += apiKeyMaxUsage[key]
keyLimit += apiKeyUsage[key].limit
}
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
}
}
/** Send an HTTP request to the Hypixel API */
export let sendApiRequest = async<P extends keyof typedHypixelApi.Requests>(path: P, options: typedHypixelApi.Requests[P]['options']): Promise<typedHypixelApi.Requests[P]['response']> => {
// Send a raw http request to api.hypixel.net, and return the parsed json
let response: typedHypixelApi.Requests[P]['response']
try {
response = await typedHypixelApi.request(
path,
options
)
} catch {
await sleep(1000)
return await sendApiRequest(path, options)
}
if (!response.data.success) {
// bruh
if (response.data.cause === 'This endpoint is currently disabled') {
await sleep(30000)
return await sendApiRequest(path, options)
}
// if the cause is "Invalid API key", remove the key from the list of keys and try again
if ('key' in options && response.data.cause === 'Invalid API key') {
if (apiKeys.includes(options.key)) {
apiKeys.splice(apiKeys.indexOf(options.key), 1)
console.log(`${options.key} is invalid, removing it from the list of keys`)
}
return await sendApiRequest(path, {
...options,
key: chooseApiKey()
})
}
}
if ('key' in options && response.headers['RateLimit-Limit']) {
// remember how many uses it has
apiKeyUsage[options.key] = {
remaining: response.headers['RateLimit-Remaining'] ?? 0,
limit: response.headers['Ratelimit-Limit'] ?? 0,
reset: Date.now() + response.headers['Ratelimit-Reset'] ?? 0 * 1000 + 1000,
}
let usage = apiKeyUsage[options.key].limit - apiKeyUsage[options.key].remaining
// if it's not in apiKeyMaxUsage or this usage is higher, update it
if (!apiKeyMaxUsage[options.key] || (usage > apiKeyMaxUsage[options.key]))
apiKeyMaxUsage[options.key] = usage
}
if ('key' in options && !response.data.success && 'throttle' in response.data && response.data.throttle) {
if (apiKeyUsage[options.key])
apiKeyUsage[options.key].remaining = 0
// if it's throttled, wait 10 seconds and try again
await sleep(10000)
return await sendApiRequest(path, options)
}
return response
}
// this is necessary for mocking in the tests because es6
export function mockSendApiRequest($value) { sendApiRequest = $value }
|