import { CleanMinion, cleanMinions, combineMinionArrays } from './cleaners/skyblock/minions' import { CleanProfileStats, cleanProfileStats } from './cleaners/skyblock/stats' import { CleanPlayer, cleanPlayerResponse } from './cleaners/player' import { chooseApiKey, HypixelPlayerStatsSkyBlockProfiles, HypixelResponse, sendApiRequest } from './hypixelApi' import * as cached from './hypixelCached' export type Included = 'profiles' | 'player' | 'stats' // the interval at which the "last_save" parameter updates in the hypixel api, this is 3 minutes export const saveInterval = 60 * 3 * 1000 // the highest level a minion can be 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 async function sendCleanApiRequest({ path, args }, included?: Included[], cleaned=true) { const key = await chooseApiKey() const rawResponse = await sendApiRequest({ path, key, args }) if (rawResponse.throttled) { // if it's throttled, wait a second and try again console.log('throttled :/') await new Promise(resolve => setTimeout(resolve, 1000)) return await sendCleanApiRequest({ path, args }, included, cleaned) } if (cleaned) { // if it needs to clean the response, call cleanResponse return await cleanResponse({ path, data: rawResponse }, included=included) } else { // this is provided in case the caller wants to do the cleaning itself // used in skyblock/profile, as cleaning the entire profile would use too much cpu return rawResponse } } export interface CleanBasicMember { uuid: string username: string last_save: number first_join: number } interface CleanMember extends CleanBasicMember { stats?: CleanProfileStats minions?: CleanMinion[] } async function cleanSkyBlockProfileMemberResponse(member, included: Included[] = null): Promise<CleanMember> { // Cleans up a member (from skyblock/profile) // profiles.members[] const statsIncluded = included == null || included.includes('stats') return { uuid: member.uuid, username: await cached.usernameFromUser(member.uuid), last_save: member.last_save, first_join: member.first_join, // last_death: ??? idk how this is formatted, stats: statsIncluded ? cleanProfileStats(member.stats) : undefined, minions: statsIncluded ? cleanMinions(member.crafted_generators) : undefined, } } export interface CleanMemberProfilePlayer extends CleanPlayer { // The profile name may be different for each player, so we put it here profileName: string first_join: number last_save: number bank?: { balance: number history: any[] } } export interface CleanMemberProfile { member: CleanMemberProfilePlayer profile: { } } export interface CleanProfile extends CleanBasicProfile { members?: CleanBasicMember[] } export interface CleanFullProfile extends CleanProfile { members: CleanMember[] bank?: { balance: number history: any[] } minions: CleanMinion[] } /** Return a `CleanProfile` instead of a `CleanFullProfile`, useful when we need to get members but don't want to waste much ram */ async function cleanSkyblockProfileResponseLighter(data): Promise<CleanProfile> { // We use Promise.all so it can fetch all the usernames at once instead of waiting for the previous promise to complete const promises: Promise<CleanMember>[] = [] for (const memberUUID in data.members) { const memberRaw = data.members[memberUUID] memberRaw.uuid = memberUUID // we pass an empty array to make it not check stats promises.push(cleanSkyBlockProfileMemberResponse(memberRaw, [])) } const cleanedMembers: CleanMember[] = await Promise.all(promises) return { uuid: data.profile_id, name: data.cute_name, members: cleanedMembers, } } /** This function is very costly and shouldn't be called often. Use cleanSkyblockProfileResponseLighter if you don't need all the data */ async function cleanSkyblockProfileResponse(data: any): Promise<CleanFullProfile> { const cleanedMembers: CleanMember[] = [] for (const memberUUID in data.members) { const memberRaw = data.members[memberUUID] memberRaw.uuid = memberUUID const member: CleanMember = await cleanSkyBlockProfileMemberResponse(memberRaw, ['stats']) cleanedMembers.push(member) } const memberMinions: CleanMinion[][] = [] for (const member of cleanedMembers) { memberMinions.push(member.minions) } const minions: CleanMinion[] = combineMinionArrays(memberMinions) // return more detailed info return { uuid: data.profile_id, name: data.cute_name, members: cleanedMembers, bank: { balance: data?.banking?.balance ?? 0, // TODO: make transactions good history: data?.banking?.transactions ?? [] }, minions } } /** A basic profile that only includes the profile uuid and name */ export interface CleanBasicProfile { uuid: string // the name depends on the user, so its sometimes not included name?: string } export function cleanPlayerSkyblockProfiles(rawProfiles: HypixelPlayerStatsSkyBlockProfiles): CleanBasicProfile[] { let profiles: CleanBasicProfile[] = [] for (const profile of Object.values(rawProfiles)) { profiles.push({ uuid: profile.profile_id, name: profile.cute_name }) } console.log('cleanPlayerSkyblockProfiles', profiles) return profiles } /** Convert an array of raw profiles into clean profiles */ async function cleanSkyblockProfilesResponse(data: any[]): Promise<CleanProfile[]> { const cleanedProfiles: CleanProfile[] = [] for (const profile of data) { let cleanedProfile = await cleanSkyblockProfileResponseLighter(profile) cleanedProfiles.push(cleanedProfile) } return cleanedProfiles } async function cleanResponse({ path, data }: { path: string, data: HypixelResponse }, included?: Included[]) { // Cleans up an api response switch (path) { case 'player': return await cleanPlayerResponse(data.player) case 'skyblock/profile': return await cleanSkyblockProfileResponse(data.profile) case 'skyblock/profiles': return await cleanSkyblockProfilesResponse(data.profiles) } } /* ----------------------------- */ export interface UserAny { user?: string uuid?: string username?: string } export interface CleanUser { player: any profiles?: any activeProfile?: string online?: boolean } /** * Higher level function that requests the api for a user, and returns the cleaned response * This is safe to fetch many times because the results are cached! * @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']): Promise<CleanUser> { if (!uuid) { // If the uuid isn't provided, get it uuid = await cached.uuidFromUser(user || username) } const includePlayers = included.includes('player') const includeProfiles = included.includes('profiles') let profilesData: CleanProfile[] let basicProfilesData: CleanBasicProfile[] let playerData: CleanPlayer if (includePlayers) { playerData = await cached.fetchPlayer(uuid) // if not including profiles, include lightweight profiles just in case if (!includeProfiles) basicProfilesData = playerData.profiles playerData.profiles = undefined } if (includeProfiles) { profilesData = await cached.fetchSkyblockProfiles(uuid) } let activeProfile: CleanProfile = null let lastOnline: number = 0 if (includeProfiles) { for (const profile of profilesData) { const member = profile.members.find(member => member.uuid === uuid) if (member.last_save > lastOnline) { lastOnline = member.last_save activeProfile = profile } } } return { player: playerData ?? null, profiles: profilesData ?? basicProfilesData, activeProfile: includeProfiles ? activeProfile?.uuid : undefined, online: includeProfiles ? lastOnline > (Date.now() - saveInterval): undefined } } /** * Fetch a CleanMemberProfile from a user and string * This is safe to use many times as the results are cached! * @param user A username or uuid * @param profile A profile name or profile uuid */ export async function fetchMemberProfile(user: string, profile: string): Promise<CleanMemberProfile> { const playerUuid = await cached.uuidFromUser(user) const profileUuid = await cached.fetchProfileUuid(user, profile) const player = await cached.fetchPlayer(playerUuid) const cleanProfile = await cached.fetchProfile(playerUuid, profileUuid) const member = cleanProfile.members.find(m => m.uuid === playerUuid) return { member: { profileName: cleanProfile.name, first_join: member.first_join, last_save: member.last_save, // add all other data relating to the hypixel player, such as username, rank, etc ...player }, profile: { minions: cleanProfile.minions } } }