diff options
-rw-r--r-- | .eslintrc.cjs | 27 | ||||
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | .prettierrc | 9 | ||||
-rw-r--r-- | .vscode/settings.json | 3 | ||||
-rw-r--r-- | package-lock.json | 48 | ||||
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | src/cleaners/player.ts | 4 | ||||
-rw-r--r-- | src/cleaners/rank.ts | 10 | ||||
-rw-r--r-- | src/cleaners/skyblock/election.ts | 2 | ||||
-rw-r--r-- | src/cleaners/skyblock/member.ts | 5 | ||||
-rw-r--r-- | src/cleaners/skyblock/profile.ts | 15 | ||||
-rw-r--r-- | src/cleaners/skyblock/profiles.ts | 13 | ||||
-rw-r--r-- | src/hypixel.ts | 88 | ||||
-rw-r--r-- | src/hypixelApi.ts | 148 | ||||
-rw-r--r-- | src/hypixelCached.ts | 16 | ||||
-rw-r--r-- | test-data-generator/index.ts | 10 |
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 @@ -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) } |