aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.cjs27
-rw-r--r--.gitignore1
-rw-r--r--.prettierrc9
-rw-r--r--.vscode/settings.json3
-rw-r--r--package-lock.json48
-rw-r--r--package.json3
-rw-r--r--src/cleaners/player.ts4
-rw-r--r--src/cleaners/rank.ts10
-rw-r--r--src/cleaners/skyblock/election.ts2
-rw-r--r--src/cleaners/skyblock/member.ts5
-rw-r--r--src/cleaners/skyblock/profile.ts15
-rw-r--r--src/cleaners/skyblock/profiles.ts13
-rw-r--r--src/hypixel.ts88
-rw-r--r--src/hypixelApi.ts148
-rw-r--r--src/hypixelCached.ts16
-rw-r--r--test-data-generator/index.ts10
16 files changed, 210 insertions, 192 deletions
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..e680809
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,27 @@
+module.exports = {
+ root: true,
+ parser: '@typescript-eslint/parser',
+ extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
+ plugins: ['svelte3', '@typescript-eslint'],
+ ignorePatterns: ['*.cjs'],
+ overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
+ settings: {
+ 'svelte3/typescript': () => require('typescript'),
+ },
+ parserOptions: {
+ sourceType: 'module',
+ ecmaVersion: 2019,
+ },
+ env: {
+ browser: true,
+ es2017: true,
+ node: true,
+ },
+ rules: {
+ '@typescript-eslint/no-non-null-assertion': 'off',
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/ban-types': 'off',
+ '@typescript-eslint/ban-ts-comment': 'off',
+
+ },
+} \ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 43016f6..a08e2da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
.env
-.vscode
node_modules
.nyc_output
coverage
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..fee6284
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,9 @@
+{
+ "printWidth": 100,
+ "useTabs": true,
+ "singleQuote": true,
+ "quoteProps": "consistent",
+ "trailingComma": "es5",
+ "semi": false,
+ "arrowParens": "avoid"
+} \ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..3b61434
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "editor.formatOnSave": true
+} \ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 8cdf084..7f07b4c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,6 +18,7 @@
"prismarine-nbt": "github:PrismarineJS/prismarine-nbt",
"prom-client": "^14.0.1",
"queue-promise": "^2.2.1",
+ "typed-hypixel-api": "^0.1.10",
"uuid": "^8.3.2"
},
"devDependencies": {
@@ -28,7 +29,7 @@
"dotenv": "^16.0.0",
"mocha": "^9.2.1",
"ts-node": "^10.5.0",
- "typescript": "^4.6.2"
+ "typescript": "^4.6.3"
},
"engines": {
"node": ">=16.0.0"
@@ -1934,11 +1935,19 @@
"node": ">= 0.6"
}
},
+ "node_modules/typed-hypixel-api": {
+ "version": "0.1.10",
+ "resolved": "https://registry.npmjs.org/typed-hypixel-api/-/typed-hypixel-api-0.1.10.tgz",
+ "integrity": "sha512-JXmD2AJtVDyFfjXSW/VQCZun5j7N0VTngO5JPXvdaQ6kxR1qjaKYGRpL8dBTQ5avDj3TaRjWwDDBgEgybZ2ugA==",
+ "dependencies": {
+ "typescript": "^4.6.2",
+ "undici": "^4.16.0"
+ }
+ },
"node_modules/typescript": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz",
- "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==",
- "dev": true,
+ "version": "4.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
+ "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -1947,6 +1956,14 @@
"node": ">=4.2.0"
}
},
+ "node_modules/undici": {
+ "version": "4.16.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-4.16.0.tgz",
+ "integrity": "sha512-tkZSECUYi+/T1i4u+4+lwZmQgLXd4BLGlrc7KZPcLIW7Jpq99+Xpc30ONv7nS6F5UNOxp/HBZSSL9MafUrvJbw==",
+ "engines": {
+ "node": ">=12.18"
+ }
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -3605,11 +3622,24 @@
"mime-types": "~2.1.24"
}
},
+ "typed-hypixel-api": {
+ "version": "0.1.10",
+ "resolved": "https://registry.npmjs.org/typed-hypixel-api/-/typed-hypixel-api-0.1.10.tgz",
+ "integrity": "sha512-JXmD2AJtVDyFfjXSW/VQCZun5j7N0VTngO5JPXvdaQ6kxR1qjaKYGRpL8dBTQ5avDj3TaRjWwDDBgEgybZ2ugA==",
+ "requires": {
+ "typescript": "^4.6.2",
+ "undici": "^4.16.0"
+ }
+ },
"typescript": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.2.tgz",
- "integrity": "sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==",
- "dev": true
+ "version": "4.6.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
+ "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw=="
+ },
+ "undici": {
+ "version": "4.16.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-4.16.0.tgz",
+ "integrity": "sha512-tkZSECUYi+/T1i4u+4+lwZmQgLXd4BLGlrc7KZPcLIW7Jpq99+Xpc30ONv7nS6F5UNOxp/HBZSSL9MafUrvJbw=="
},
"unpipe": {
"version": "1.0.0",
diff --git a/package.json b/package.json
index d624d06..05b66e1 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"prismarine-nbt": "github:PrismarineJS/prismarine-nbt",
"prom-client": "^14.0.1",
"queue-promise": "^2.2.1",
+ "typed-hypixel-api": "^0.1.10",
"uuid": "^8.3.2"
},
"devDependencies": {
@@ -48,7 +49,7 @@
"dotenv": "^16.0.0",
"mocha": "^9.2.1",
"ts-node": "^10.5.0",
- "typescript": "^4.6.2"
+ "typescript": "^4.6.3"
},
"type": "module"
}
diff --git a/src/cleaners/player.ts b/src/cleaners/player.ts
index 1fd6f85..6467e59 100644
--- a/src/cleaners/player.ts
+++ b/src/cleaners/player.ts
@@ -1,8 +1,8 @@
+import { PlayerDataResponse as HypixelApiPlayerDataResponse } from 'typed-hypixel-api/build/responses/player'
import { cleanPlayerSkyblockProfiles } from './skyblock/profiles.js'
import { cleanSocialMedia, CleanSocialMedia } from './socialmedia.js'
import { CleanBasicProfile } from './skyblock/profile.js'
import { cleanRank, CleanRank } from './rank.js'
-import { HypixelPlayer } from '../hypixelApi.js'
import { undashUuid } from '../util.js'
export interface CleanBasicPlayer {
@@ -16,7 +16,7 @@ export interface CleanPlayer extends CleanBasicPlayer {
profiles?: CleanBasicProfile[]
}
-export async function cleanPlayerResponse(data: HypixelPlayer): Promise<CleanPlayer | null> {
+export async function cleanPlayerResponse(data: HypixelApiPlayerDataResponse['player']): Promise<CleanPlayer | null> {
// Cleans up a 'player' api response
if (!data)
return null // bruh
diff --git a/src/cleaners/rank.ts b/src/cleaners/rank.ts
index 417c8f1..ea7579e 100644
--- a/src/cleaners/rank.ts
+++ b/src/cleaners/rank.ts
@@ -1,7 +1,7 @@
+import { PlayerDataResponse as HypixelApiPlayerDataResponse } from 'typed-hypixel-api/build/responses/player'
import { colorCodeFromName, minecraftColorCodes } from '../util.js'
-import { HypixelPlayer } from '../hypixelApi.js'
-const rankColors: { [ name: string ]: string } = {
+const rankColors: { [name: string]: string } = {
'NONE': '7',
'VIP': 'a',
'VIP+': 'a',
@@ -29,7 +29,7 @@ export function cleanRank({
rankPlusColor,
rank,
prefix
-}: HypixelPlayer): CleanRank {
+}: HypixelApiPlayerDataResponse['player']): CleanRank {
let name: string | undefined
let color: string
let colored: string
@@ -48,7 +48,7 @@ export function cleanRank({
?? packageRank?.replace('_PLUS', '+')
switch (name) {
- // MVP++ is called Superstar for some reason
+ // MVP++ is called Superstar for some reason
case 'SUPERSTAR':
name = 'MVP++'
break
@@ -87,7 +87,7 @@ export function cleanRank({
if (bracketColor)
colored = `§${bracketColor}[${rankColorPrefix}${name}§${bracketColor}]`
else
- colored = `${rankColorPrefix}[${name}]`
+ colored = `${rankColorPrefix}[${name}]`
else
// nons don't have a prefix
colored = `${rankColorPrefix}`
diff --git a/src/cleaners/skyblock/election.ts b/src/cleaners/skyblock/election.ts
index a773f4a..9094e2d 100644
--- a/src/cleaners/skyblock/election.ts
+++ b/src/cleaners/skyblock/election.ts
@@ -39,7 +39,7 @@ function cleanCandidate(data: any, index: number): Candidate {
}
}
-export function cleanElectionResponse(data: any): ElectionData {
+export async function cleanElectionResponse(data: any): Promise<ElectionData> {
const previousCandidates = data.mayor.election.candidates.map(cleanCandidate)
return {
lastUpdated: data.lastUpdated,
diff --git a/src/cleaners/skyblock/member.ts b/src/cleaners/skyblock/member.ts
index 3430eac..e187beb 100644
--- a/src/cleaners/skyblock/member.ts
+++ b/src/cleaners/skyblock/member.ts
@@ -1,8 +1,9 @@
+import { ProfileMember as HypixelApiProfileMember } from 'typed-hypixel-api/build/responses/skyblock/_profile_member'
import { cleanCollections, Collection } from './collections.js'
import { cleanInventories, Inventories } from './inventory.js'
import { cleanFairySouls, FairySouls } from './fairysouls.js'
import { cleanObjectives, Objective } from './objectives.js'
-import { CleanBasicProfile, CleanFullProfileBasicMembers } from './profile.js'
+import { CleanFullProfileBasicMembers } from './profile.js'
import { cleanProfileStats, StatItem } from './stats.js'
import { CleanMinion, cleanMinions } from './minions.js'
import { cleanSlayers, SlayerData } from './slayers.js'
@@ -53,7 +54,7 @@ export async function cleanSkyBlockProfileMemberResponseBasic(member: any): Prom
}
/** Cleans up a member (from skyblock/profile) */
-export async function cleanSkyBlockProfileMemberResponse(member, profileId?: string, included: Included[] | undefined = undefined): Promise<CleanMember | null> {
+export async function cleanSkyBlockProfileMemberResponse(member: HypixelApiProfileMember & { uuid: string }, profileId?: string, included: Included[] | undefined = undefined): Promise<CleanMember | null> {
// profiles.members[]
const inventoriesIncluded = included === undefined || included.includes('inventories')
const player = await cached.fetchPlayer(member.uuid)
diff --git a/src/cleaners/skyblock/profile.ts b/src/cleaners/skyblock/profile.ts
index ed85861..03ff2eb 100644
--- a/src/cleaners/skyblock/profile.ts
+++ b/src/cleaners/skyblock/profile.ts
@@ -1,4 +1,5 @@
import { CleanBasicMember, CleanMember, cleanSkyBlockProfileMemberResponse, cleanSkyBlockProfileMemberResponseBasic } from './member.js'
+import { SkyBlockProfilesResponse as HypixelApiSkyBlockProfilesResponse } from 'typed-hypixel-api/build/responses/skyblock/profiles'
import { CleanMinion, combineMinionArrays, countUniqueMinions } from './minions.js'
import * as constants from '../../constants.js'
import { ApiOptions } from '../../hypixel.js'
@@ -48,7 +49,7 @@ export async function cleanSkyblockProfileResponseLighter(data): Promise<CleanPr
/**
* This function is somewhat costly and shouldn't be called often. Use cleanSkyblockProfileResponseLighter if you don't need all the data
*/
-export async function cleanSkyblockProfileResponse(data: any, options?: ApiOptions): Promise<CleanFullProfile | CleanProfile | null> {
+export async function cleanSkyblockProfileResponse<O extends ApiOptions>(data: HypixelApiSkyBlockProfilesResponse['profiles'][number], options?: O): Promise<(O['basic'] extends true ? CleanProfile : CleanFullProfile) | null> {
// We use Promise.all so it can fetch all the users at once instead of waiting for the previous promise to complete
const promises: Promise<CleanMember | null>[] = []
if (!data) return null
@@ -57,9 +58,9 @@ export async function cleanSkyblockProfileResponse(data: any, options?: ApiOptio
for (const memberUUID in data.members) {
const memberRaw = data.members[memberUUID]
- memberRaw.uuid = memberUUID
+ const memberRawWithUuid = { ...memberRaw, uuid: memberUUID }
promises.push(cleanSkyBlockProfileMemberResponse(
- memberRaw,
+ memberRawWithUuid,
profileId,
[
!options?.basic ? 'stats' : undefined,
@@ -72,11 +73,13 @@ export async function cleanSkyblockProfileResponse(data: any, options?: ApiOptio
const cleanedMembers: CleanMember[] = (await Promise.all(promises)).filter(m => m) as CleanMember[]
if (options?.basic) {
- return {
+ const cleanProfile: CleanProfile = {
uuid: profileId,
name: data.cute_name,
members: cleanedMembers,
}
+ // we have to do this because of the basic checking typing
+ return cleanProfile as any
}
const memberMinions: CleanMinion[][] = []
@@ -93,7 +96,7 @@ export async function cleanSkyblockProfileResponse(data: any, options?: ApiOptio
await constants.setConstantValues({ max_minions: uniqueMinions })
// return more detailed info
- return {
+ const cleanFullProfile: CleanFullProfile = {
uuid: data.profile_id,
name: data.cute_name,
members: cleanedMembers,
@@ -102,6 +105,8 @@ export async function cleanSkyblockProfileResponse(data: any, options?: ApiOptio
minionCount: uniqueMinions,
maxUniqueMinions: maxUniqueMinions ?? 0,
}
+ // we have to do this because of the basic checking typing
+ return cleanFullProfile as any
}
/** A basic profile that only includes the profile uuid and name */
diff --git a/src/cleaners/skyblock/profiles.ts b/src/cleaners/skyblock/profiles.ts
index 20c2104..ddab078 100644
--- a/src/cleaners/skyblock/profiles.ts
+++ b/src/cleaners/skyblock/profiles.ts
@@ -5,8 +5,11 @@ import {
CleanProfile,
cleanSkyblockProfileResponse
} from './profile.js'
+import { SkyBlockProfilesResponse } from 'typed-hypixel-api/build/responses/skyblock/profiles'
+
+export function cleanPlayerSkyblockProfiles(rawProfiles: HypixelPlayerStatsSkyBlockProfiles | undefined): CleanBasicProfile[] {
+ if (!rawProfiles) return []
-export function cleanPlayerSkyblockProfiles(rawProfiles: HypixelPlayerStatsSkyBlockProfiles): CleanBasicProfile[] {
let profiles: CleanBasicProfile[] = []
for (const profile of Object.values(rawProfiles ?? {})) {
profiles.push({
@@ -18,11 +21,11 @@ export function cleanPlayerSkyblockProfiles(rawProfiles: HypixelPlayerStatsSkyBl
}
/** Convert an array of raw profiles into clean profiles */
-export async function cleanSkyblockProfilesResponse(data: any[]): Promise<CleanProfile[]> {
- const promises: Promise<CleanProfile | CleanFullProfile | null>[] = []
- for (const profile of data ?? []) {
+export async function cleanSkyblockProfilesResponse(data: SkyBlockProfilesResponse['profiles']): Promise<CleanFullProfile[]> {
+ const promises: Promise<CleanFullProfile | null>[] = []
+ for (const profile of data) {
promises.push(cleanSkyblockProfileResponse(profile))
}
- const cleanedProfiles: CleanProfile[] = (await Promise.all(promises)).filter(p => p) as CleanProfile[]
+ const cleanedProfiles: CleanFullProfile[] = (await Promise.all(promises)).filter((p): p is CleanFullProfile => p !== null)
return cleanedProfiles
}
diff --git a/src/hypixel.ts b/src/hypixel.ts
index 6691e97..4ae299a 100644
--- a/src/hypixel.ts
+++ b/src/hypixel.ts
@@ -16,15 +16,15 @@ import {
queueUpdateDatabaseMember,
queueUpdateDatabaseProfile
} from './database.js'
+import { cleanElectionResponse, ElectionData } from './cleaners/skyblock/election.js'
import { CleanBasicMember, CleanMemberProfile } from './cleaners/skyblock/member.js'
-import { chooseApiKey, HypixelResponse, sendApiRequest } from './hypixelApi.js'
import { cleanSkyblockProfilesResponse } from './cleaners/skyblock/profiles.js'
import { CleanPlayer, cleanPlayerResponse } from './cleaners/player.js'
+import { chooseApiKey, sendApiRequest } from './hypixelApi.js'
+import typedHypixelApi from 'typed-hypixel-api'
import * as cached from './hypixelCached.js'
import { debug } from './index.js'
-import { sleep } from './util.js'
import { WithId } from 'mongodb'
-import { cleanElectionResponse, ElectionData } from './cleaners/skyblock/election.js'
export type Included = 'profiles' | 'player' | 'stats' | 'inventories' | undefined
@@ -36,7 +36,7 @@ export const maxMinion = 11
/**
* Send a request to api.hypixel.net using a random key, clean it up to be more useable, and return it
- */
+ */
export interface ApiOptions {
mainMemberUuid?: string
@@ -45,30 +45,29 @@ export interface ApiOptions {
}
/** Sends an API request to Hypixel and cleans it up. */
-export async function sendCleanApiRequest({ path, args }, included?: Included[], options?: ApiOptions): Promise<any> {
+export async function sendCleanApiRequest<P extends keyof typeof cleanResponseFunctions>(path: P, args: Omit<typedHypixelApi.Requests[P]['options'], 'key'>, options?: ApiOptions): Promise<Awaited<ReturnType<typeof cleanResponseFunctions[P]>>> {
const key = await chooseApiKey()
- const rawResponse = await sendApiRequest({ path, key, args })
- if (rawResponse.throttled) {
- // if it's throttled, wait a second and try again
- await sleep(1000)
- return await sendCleanApiRequest({ path, args }, included, options)
- }
+ const data = await sendApiRequest(path, { key, ...args })
// clean the response
- return await cleanResponse({ path, data: rawResponse }, options ?? {})
+ return await cleanResponse(path, data, options ?? {})
}
+const cleanResponseFunctions = {
+ 'player': (data, options) => cleanPlayerResponse(data.player),
+ 'skyblock/profile': (data, options) => cleanSkyblockProfileResponse(data.profile, options),
+ 'skyblock/profiles': (data, options) => cleanSkyblockProfilesResponse(data.profiles),
+ 'resources/skyblock/election': (data, options) => cleanElectionResponse(data)
+} as const
-async function cleanResponse({ path, data }: { path: string, data: HypixelResponse }, options: ApiOptions): Promise<any> {
+async function cleanResponse<P extends keyof typeof cleanResponseFunctions>(path: P, data: typedHypixelApi.Requests[P]['response'], options: ApiOptions): Promise<Awaited<ReturnType<typeof cleanResponseFunctions[P]>>> {
// Cleans up an api response
- switch (path) {
- case 'player': return await cleanPlayerResponse(data.player)
- case 'skyblock/profile': return await cleanSkyblockProfileResponse(data.profile, options)
- case 'skyblock/profiles': return await cleanSkyblockProfilesResponse(data.profiles)
- case 'resources/skyblock/election': return await cleanElectionResponse(data)
- }
+ const cleaningFunction: typeof cleanResponseFunctions[P] = cleanResponseFunctions[path]
+ const cleanedData = await cleaningFunction(data, options)
+ return cleanedData as Awaited<ReturnType<typeof cleanResponseFunctions[P]>>
}
+
/* ----------------------------- */
export interface UserAny {
@@ -93,13 +92,13 @@ export interface CleanUser {
* @param included lets you choose what is returned, so there's less processing required on the backend.
* used inclusions: player, profiles
*/
-export async function fetchUser({ user, uuid, username }: UserAny, included: Included[]=['player'], customization?: boolean): Promise<CleanUser | null> {
+export async function fetchUser({ user, uuid, username }: UserAny, included: Included[] = ['player'], customization?: boolean): Promise<CleanUser | null> {
if (!uuid) {
// If the uuid isn't provided, get it
if (!username && !user) return null
uuid = await cached.uuidFromUser((user ?? username)!)
}
- if (!uuid) {
+ if (!uuid) {
// the user doesn't exist.
if (debug) console.debug('error:', user, 'doesnt exist')
return null
@@ -146,7 +145,7 @@ export async function fetchUser({ user, uuid, username }: UserAny, included: Inc
player: playerData,
profiles: profilesData ?? basicProfilesData,
activeProfile: includeProfiles ? activeProfile!?.uuid : undefined,
- online: includeProfiles ? lastOnline > (Date.now() - saveInterval): undefined,
+ online: includeProfiles ? lastOnline > (Date.now() - saveInterval) : undefined,
customization: websiteAccount?.customization
}
}
@@ -219,19 +218,20 @@ export async function fetchMemberProfile(user: string, profile: string, customiz
* @param playerUuid The UUID of the Minecraft player
* @param profileUuid The UUID of the Hypixel SkyBlock profile
*/
- export async function fetchMemberProfileUncached(playerUuid: string, profileUuid: string): Promise<CleanFullProfile> {
- const profile: CleanFullProfile = await sendCleanApiRequest(
- {
- path: 'skyblock/profile',
- args: { profile: profileUuid }
- },
- undefined,
+export async function fetchMemberProfileUncached(playerUuid: string, profileUuid: string): Promise<null | CleanFullProfile> {
+ const profile = await sendCleanApiRequest(
+ 'skyblock/profile',
+ { profile: profileUuid },
{ mainMemberUuid: playerUuid }
)
+ // we check for minions in profile to filter out the CleanProfile type (as opposed to CleanFullProfile)
+ if (!profile || !('minions' in profile)) return null
+
// queue updating the leaderboard positions for the member, eventually
- for (const member of profile.members)
- queueUpdateDatabaseMember(member, profile)
+ if (profile.members)
+ for (const member of profile.members)
+ queueUpdateDatabaseMember(member, profile)
queueUpdateDatabaseProfile(profile)
return profile
@@ -242,13 +242,10 @@ export async function fetchMemberProfile(user: string, profile: string, customiz
* @param playerUuid The UUID of the Minecraft player
* @param profileUuid The UUID of the Hypixel SkyBlock profile
*/
- export async function fetchBasicProfileFromUuidUncached(profileUuid: string): Promise<CleanProfile> {
- const profile: CleanFullProfile = await sendCleanApiRequest(
- {
- path: 'skyblock/profile',
- args: { profile: profileUuid }
- },
- undefined,
+export async function fetchBasicProfileFromUuidUncached(profileUuid: string): Promise<CleanProfile | null> {
+ const profile = await sendCleanApiRequest(
+ 'skyblock/profile',
+ { profile: profileUuid },
{ basic: true }
)
@@ -258,11 +255,8 @@ export async function fetchMemberProfile(user: string, profile: string, customiz
export async function fetchMemberProfilesUncached(playerUuid: string): Promise<CleanFullProfile[]> {
const profiles: CleanFullProfile[] = await sendCleanApiRequest(
- {
- path: 'skyblock/profiles',
- args: { uuid: playerUuid }
- },
- undefined,
+ 'skyblock/profiles',
+ { uuid: playerUuid },
{
// only the inventories for the main player are generated, this is for optimization purposes
mainMemberUuid: playerUuid
@@ -299,10 +293,10 @@ export async function fetchElection(): Promise<ElectionData> {
}
isFetchingElection = true
- const election: ElectionData = await sendCleanApiRequest({
- path: 'resources/skyblock/election',
- args: {}
- })
+ const election: ElectionData = await sendCleanApiRequest(
+ 'resources/skyblock/election',
+ {}
+ )
isFetchingElection = false
cachedElectionData = election
diff --git a/src/hypixelApi.ts b/src/hypixelApi.ts
index 13866f9..186cec6 100644
--- a/src/hypixelApi.ts
+++ b/src/hypixelApi.ts
@@ -1,22 +1,16 @@
/**
* Fetch the raw Hypixel API
*/
-import { jsonToQuery, shuffle, sleep } from './util.js'
-import * as nodeFetch from 'node-fetch'
-import fetch from 'node-fetch'
+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()
-// 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 :) */
+/** 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 {
@@ -25,11 +19,9 @@ interface KeyUsage {
reset: number
}
-const apiKeyUsage: { [ key: string ]: KeyUsage } = {}
+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'
+const apiKeyMaxUsage: { [key: string]: number } = {}
/** Choose the best current API key */
@@ -77,14 +69,14 @@ export function getKeyUsage() {
export interface HypixelResponse {
[key: string]: any | {
- success: boolean
- throttled?: boolean
- }
+ success: boolean
+ throttled?: boolean
+ }
}
export interface HypixelPlayerStatsSkyBlockProfiles {
- [ uuid: string ]: {
+ [uuid: string]: {
profile_id: string
cute_name: string
}
@@ -103,112 +95,66 @@ export interface HypixelPlayerSocialMedia {
}
}
-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> {
+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
- 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
-
+ let response: typedHypixelApi.Requests[P]['response']
try {
- fetchResponse = await fetch(
- fetchUrl,
- { agent: () => httpsAgent }
+ response = await typedHypixelApi.request(
+ path,
+ options
)
- fetchJsonParsed = await fetchResponse.json()
} catch {
- // if there's an error, wait a second and try again
await sleep(1000)
- return await sendApiRequest({ path, key, args })
+ return await sendApiRequest(path, options)
}
- // bruh
- if (fetchJsonParsed.cause === 'This endpoint is currently disabled') {
- await sleep(30000)
- return await sendApiRequest({ path, key, args })
- }
+ 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 (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`)
+ // 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()
+ })
}
- return await sendApiRequest({ path, key: chooseApiKey(), args })
}
- if (fetchResponse.headers.get('ratelimit-limit')) {
+ if ('key' in options && response.headers['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,
+ 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[key].limit - apiKeyUsage[key].remaining
+ 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[key] || (usage > apiKeyMaxUsage[key]))
- apiKeyMaxUsage[key] = usage
+ if (!apiKeyMaxUsage[options.key] || (usage > apiKeyMaxUsage[options.key]))
+ apiKeyMaxUsage[options.key] = usage
}
-
- if (fetchJsonParsed.throttle) {
- if (apiKeyUsage[key])
- apiKeyUsage[key].remaining = 0
+
+ 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, key, args })
+ return await sendApiRequest(path, options)
}
- return fetchJsonParsed
+ return response
}
// this is necessary for mocking in the tests because es6
-export function mockSendApiRequest($value) { sendApiRequest = $value } \ No newline at end of file
+export function mockSendApiRequest($value) { sendApiRequest = $value }
diff --git a/src/hypixelCached.ts b/src/hypixelCached.ts
index bc34ef0..5a17c3c 100644
--- a/src/hypixelCached.ts
+++ b/src/hypixelCached.ts
@@ -174,10 +174,9 @@ export async function fetchPlayer(user: string): Promise<CleanPlayer | null> {
fetchingPlayers.add(playerUuid)
- const cleanPlayer: CleanPlayer = await hypixel.sendCleanApiRequest({
- path: 'player',
- args: { uuid: playerUuid }
- })
+ const cleanPlayer = await hypixel.sendCleanApiRequest('player',
+ { uuid: playerUuid }
+ )
fetchingPlayers.delete(playerUuid)
@@ -185,7 +184,7 @@ export async function fetchPlayer(user: string): Promise<CleanPlayer | null> {
playerCache.set(playerUuid, cleanPlayer)
usernameCache.set(playerUuid, cleanPlayer.username)
-
+
// clone in case it gets modified somehow later
const cleanBasicPlayer = Object.assign({}, cleanPlayer)
if (cleanBasicPlayer.profiles) {
@@ -335,7 +334,8 @@ export async function fetchProfile(user: string, profile: string): Promise<Clean
if (!profileName) return null // uhh this should never happen but if it does just return null
- const cleanProfile: CleanFullProfile = await hypixel.fetchMemberProfileUncached(playerUuid, profileUuid)
+ const cleanProfile = await hypixel.fetchMemberProfileUncached(playerUuid, profileUuid)
+ if (!cleanProfile) return null
// we know the name from fetchProfileName, so set it here
cleanProfile.name = profileName
@@ -349,12 +349,12 @@ export async function fetchProfile(user: string, profile: string): Promise<Clean
* Fetch a CleanProfile from the uuid
* @param profileUuid A profile name or profile uuid
*/
-export async function fetchBasicProfileFromUuid(profileUuid: string): Promise<CleanProfile | undefined> {
+export async function fetchBasicProfileFromUuid(profileUuid: string): Promise<CleanProfile | null> {
if (profileCache.has(profileUuid)) {
// we have the profile cached, return it :)
if (debug) console.debug('Cache hit! fetchBasicProfileFromUuid', profileUuid)
const profile: CleanFullProfile | undefined = profileCache.get(profileUuid)
- if (!profile) return undefined
+ if (!profile) return null
return {
uuid: profile.uuid,
members: profile.members.map(m => ({
diff --git a/test-data-generator/index.ts b/test-data-generator/index.ts
index 18cefd7..f296c16 100644
--- a/test-data-generator/index.ts
+++ b/test-data-generator/index.ts
@@ -4,6 +4,7 @@
globalThis.isTest = true
+import typedHypixelApi from 'typed-hypixel-api'
import * as hypixelApi from '../src/hypixelApi'
import * as constants from '../src/constants'
import * as mojang from '../src/mojang'
@@ -24,11 +25,10 @@ async function writeTestData(requestPath: string, name: string, contents: any) {
await fs.writeFile(path.join(dir, `${name}.json`), JSON.stringify(contents, null, 2))
}
-async function addResponse(requestPath: string, args: { [ key: string ]: string }, name: string) {
- const response = await hypixelApi.sendApiRequest({
- path: requestPath,
- args: args,
- key: hypixelApi.chooseApiKey()
+async function addResponse(requestPath: keyof typedHypixelApi.Requests, args: { [ key: string ]: string }, name: string) {
+ const response = await hypixelApi.sendApiRequest(requestPath, {
+ key: hypixelApi.chooseApiKey(),
+ ...args,
})
await writeTestData(requestPath, name, response)
}