aboutsummaryrefslogtreecommitdiff
path: root/src/hypixelApi.ts
blob: 7b6a4a806097a436d9dc5e90e4f8b930fc895ba0 (plain)
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
209
210
211
212
213
214
/**
 * 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.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
	}
}

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 + 1000,
		}

		let usage = apiKeyUsage[key].limit - apiKeyUsage[key].remaining
		// if it's not in apiKeyMaxUsage or this usage is higher, update it
		if (!apiKeyMaxUsage[key] || (usage > apiKeyMaxUsage[key]))
			apiKeyMaxUsage[key] = usage
	}
	
	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 }