From 342d31e99d1fbd7bcc941e34bb2a175382f3e26e Mon Sep 17 00:00:00 2001 From: mat Date: Thu, 30 Dec 2021 16:18:57 -0600 Subject: remove build folder --- build/cleaners/player.js | 17 - build/cleaners/rank.js | 81 ----- build/cleaners/skyblock/bank.js | 8 - build/cleaners/skyblock/collections.js | 116 ------- build/cleaners/skyblock/fairysouls.js | 7 - build/cleaners/skyblock/inventory.js | 83 ----- build/cleaners/skyblock/itemId.js | 57 ---- build/cleaners/skyblock/member.js | 54 --- build/cleaners/skyblock/minions.js | 85 ----- build/cleaners/skyblock/objectives.js | 12 - build/cleaners/skyblock/profile.js | 64 ---- build/cleaners/skyblock/profiles.js | 21 -- build/cleaners/skyblock/skills.js | 146 -------- build/cleaners/skyblock/slayers.js | 60 ---- build/cleaners/skyblock/stats.js | 91 ----- build/cleaners/skyblock/zones.js | 23 -- build/cleaners/socialmedia.js | 6 - build/constants.js | 226 ------------- build/database.js | 600 --------------------------------- build/discord.js | 37 -- build/hypixel.js | 190 ----------- build/hypixelApi.js | 101 ------ build/hypixelCached.js | 339 ------------------- build/index.js | 169 ---------- build/mojang.js | 106 ------ build/util.js | 70 ---- 26 files changed, 2769 deletions(-) delete mode 100644 build/cleaners/player.js delete mode 100644 build/cleaners/rank.js delete mode 100644 build/cleaners/skyblock/bank.js delete mode 100644 build/cleaners/skyblock/collections.js delete mode 100644 build/cleaners/skyblock/fairysouls.js delete mode 100644 build/cleaners/skyblock/inventory.js delete mode 100644 build/cleaners/skyblock/itemId.js delete mode 100644 build/cleaners/skyblock/member.js delete mode 100644 build/cleaners/skyblock/minions.js delete mode 100644 build/cleaners/skyblock/objectives.js delete mode 100644 build/cleaners/skyblock/profile.js delete mode 100644 build/cleaners/skyblock/profiles.js delete mode 100644 build/cleaners/skyblock/skills.js delete mode 100644 build/cleaners/skyblock/slayers.js delete mode 100644 build/cleaners/skyblock/stats.js delete mode 100644 build/cleaners/skyblock/zones.js delete mode 100644 build/cleaners/socialmedia.js delete mode 100644 build/constants.js delete mode 100644 build/database.js delete mode 100644 build/discord.js delete mode 100644 build/hypixel.js delete mode 100644 build/hypixelApi.js delete mode 100644 build/hypixelCached.js delete mode 100644 build/index.js delete mode 100644 build/mojang.js delete mode 100644 build/util.js (limited to 'build') diff --git a/build/cleaners/player.js b/build/cleaners/player.js deleted file mode 100644 index 214be1f..0000000 --- a/build/cleaners/player.js +++ /dev/null @@ -1,17 +0,0 @@ -import { cleanPlayerSkyblockProfiles } from './skyblock/profiles.js'; -import { cleanSocialMedia } from './socialmedia.js'; -import { cleanRank } from './rank.js'; -import { undashUuid } from '../util.js'; -export async function cleanPlayerResponse(data) { - // Cleans up a 'player' api response - if (!data) - return null; // bruh - return { - uuid: undashUuid(data.uuid), - username: data.displayname, - rank: cleanRank(data), - socials: cleanSocialMedia(data), - // first_join: data.firstLogin / 1000, - profiles: cleanPlayerSkyblockProfiles(data.stats?.SkyBlock?.profiles) - }; -} diff --git a/build/cleaners/rank.js b/build/cleaners/rank.js deleted file mode 100644 index 1d80d2c..0000000 --- a/build/cleaners/rank.js +++ /dev/null @@ -1,81 +0,0 @@ -import { colorCodeFromName, minecraftColorCodes } from '../util.js'; -const rankColors = { - 'NONE': '7', - 'VIP': 'a', - 'VIP+': 'a', - 'MVP': 'b', - 'MVP+': 'b', - 'MVP++': '6', - 'YOUTUBE': 'c', - 'HELPER': '9', - 'MOD': '2', - 'GM': '2', - 'ADMIN': 'c' -}; -/** Response cleaning (reformatting to be nicer) */ -export function cleanRank({ packageRank, newPackageRank, monthlyPackageRank, rankPlusColor, rank, prefix }) { - let name; - let color; - let colored; - let bracketColor; - if (prefix) { // derive values from prefix - colored = prefix; - color = minecraftColorCodes[colored.match(/§./)[0][1]]; - name = colored.replace(/§./g, '').replace(/[\[\]]/g, ''); - } - else { - if (monthlyPackageRank && monthlyPackageRank !== 'NONE') - name = monthlyPackageRank; - else if (rank && rank !== 'NORMAL') - name = rank; - else - name = newPackageRank?.replace('_PLUS', '+') - ?? packageRank?.replace('_PLUS', '+'); - switch (name) { - // MVP++ is called Superstar for some reason - case 'SUPERSTAR': - name = 'MVP++'; - break; - // YouTube rank is called YouTuber, change this to the proper name - case 'YOUTUBER': - name = 'YOUTUBE'; - bracketColor = 'c'; - break; - case 'GAME_MASTER': - name = 'GM'; - break; - case 'MODERATOR': - name = 'MOD'; - break; - case undefined: - name = 'NONE'; - break; - } - const plusColor = rankPlusColor ? colorCodeFromName(rankPlusColor) : null; - color = minecraftColorCodes[rankColors[name]]; - let rankColorPrefix = rankColors[name] ? '§' + rankColors[name] : ''; - // the text is white, but only in the prefix - if (name === 'YOUTUBE') - rankColorPrefix = '§f'; - const nameWithoutPlus = name.split('+')[0]; - const plusesInName = '+'.repeat(name.split('+').length - 1); - if (plusColor && plusesInName.length >= 1) - if (bracketColor) - colored = `§${bracketColor}[${rankColorPrefix}${nameWithoutPlus}§${plusColor}${plusesInName}${rankColorPrefix}§${bracketColor}]`; - else - colored = `${rankColorPrefix}[${nameWithoutPlus}§${plusColor}${plusesInName}${rankColorPrefix}]`; - else if (name !== 'NONE') - if (bracketColor) - colored = `§${bracketColor}[${rankColorPrefix}${name}§${bracketColor}]`; - else - colored = `${rankColorPrefix}[${name}]`; - else - // nons don't have a prefix - colored = `${rankColorPrefix}`; - } - return { - name, - color, - colored - }; -} diff --git a/build/cleaners/skyblock/bank.js b/build/cleaners/skyblock/bank.js deleted file mode 100644 index fb08af5..0000000 --- a/build/cleaners/skyblock/bank.js +++ /dev/null @@ -1,8 +0,0 @@ -export function cleanBank(data) { - return { - balance: data?.banking?.balance ?? 0, - // TODO: make transactions good - // history: data?.banking?.transactions ?? [] - history: [] - }; -} diff --git a/build/cleaners/skyblock/collections.js b/build/cleaners/skyblock/collections.js deleted file mode 100644 index 4a5b34e..0000000 --- a/build/cleaners/skyblock/collections.js +++ /dev/null @@ -1,116 +0,0 @@ -import { cleanItemId } from './itemId.js'; -const COLLECTIONS = { - 'farming': [ - 'wheat', - 'carrot', - 'potato', - 'pumpkin', - 'melon_slice', - 'wheat_seeds', - 'red_mushroom', - 'cocoa_beans', - 'cactus', - 'sugar_cane', - 'feather', - 'leather', - 'porkchop', - 'chicken', - 'mutton', - 'rabbit', - 'nether_wart' - ], - 'mining': [ - 'cobblestone', - 'coal', - 'iron_ingot', - 'gold_ingot', - 'diamond', - 'lapis_lazuli', - 'emerald', - 'redstone', - 'quartz', - 'obsidian', - 'glowstone_dust', - 'gravel', - 'ice', - 'netherrack', - 'sand', - 'end_stone' - ], - 'combat': [ - 'rotten_flesh', - 'bone', - 'string', - 'spider_eye', - 'gunpowder', - 'ender_pearl', - 'ghast_tear', - 'slime_ball', - 'blaze_rod', - 'magma_cream' - ], - 'foraging': [ - 'oak_log', - 'spruce_log', - 'birch_log', - 'jungle_log', - 'acacia_log', - 'dark_oak_log' - ], - 'fishing': [ - 'cod', - 'salmon', - 'tropical_fish', - 'pufferfish', - 'prismarine_shard', - 'prismarine_crystals', - 'clay_ball', - 'lily_pad', - 'ink_sac', - 'sponge' - ], - // no item should be here, but in case a new collection is added itll default to this - 'unknown': [] -}; -// get a category name (farming) from a collection name (wheat) -function getCategory(collectionName) { - for (const categoryName in COLLECTIONS) { - const categoryItems = COLLECTIONS[categoryName]; - if (categoryItems.includes(collectionName)) - return categoryName; - } -} -export function cleanCollections(data) { - // collection tiers show up like this: [ GRAVEL_3, GOLD_INGOT_2, MELON_-1, LOG_2:1_7, RAW_FISH:3_-1] - // these tiers are the same for all players in a coop - const playerCollectionTiersRaw = data?.unlocked_coll_tiers ?? []; - const playerCollectionTiers = {}; - for (const collectionTierNameValueRaw of playerCollectionTiersRaw) { - const [collectionTierNameRaw, collectionTierValueRaw] = collectionTierNameValueRaw.split(/_(?=-?\d+$)/); - const collectionName = cleanItemId(collectionTierNameRaw); - // ensure it's at least 0 - const collectionValue = Math.max(parseInt(collectionTierValueRaw), 0); - // if the collection hasn't been checked yet, or the new value is higher than the old, replace it - if (!playerCollectionTiers[collectionName] || collectionValue > playerCollectionTiers[collectionName]) - playerCollectionTiers[collectionName] = collectionValue; - } - // collection names show up like this: { LOG: 49789, LOG:2: 26219, MUSHROOM_COLLECTION: 2923} - // these values are different for each player in a coop - const playerCollectionXpsRaw = data?.collection ?? {}; - const playerCollections = []; - for (const collectionNameRaw in playerCollectionXpsRaw) { - const collectionXp = playerCollectionXpsRaw[collectionNameRaw]; - const collectionName = cleanItemId(collectionNameRaw); - const collectionLevel = playerCollectionTiers[collectionName]; - const collectionCategory = getCategory(collectionName) ?? 'unknown'; - // in some very weird cases the collection level will be undefined, we should ignore these collections - if (collectionLevel !== undefined) - playerCollections.push({ - name: collectionName, - xp: collectionXp, - level: collectionLevel, - category: collectionCategory - }); - } - return playerCollections; -} diff --git a/build/cleaners/skyblock/fairysouls.js b/build/cleaners/skyblock/fairysouls.js deleted file mode 100644 index 8ec8078..0000000 --- a/build/cleaners/skyblock/fairysouls.js +++ /dev/null @@ -1,7 +0,0 @@ -export function cleanFairySouls(data) { - return { - total: data?.fairy_souls_collected ?? 0, - unexchanged: data?.fairy_souls ?? 0, - exchanges: data?.fairy_exchanges ?? 0, - }; -} diff --git a/build/cleaners/skyblock/inventory.js b/build/cleaners/skyblock/inventory.js deleted file mode 100644 index 714a302..0000000 --- a/build/cleaners/skyblock/inventory.js +++ /dev/null @@ -1,83 +0,0 @@ -// maybe todo?: create a fast replacement for prismarine-nbt -import * as nbt from 'prismarine-nbt'; -function base64decode(base64) { - return Buffer.from(base64, 'base64'); -} -function cleanItem(rawItem) { - // if the item doesn't have an id, it isn't an item - if (rawItem.id === undefined) - return null; - const vanillaId = rawItem.id; - const itemCount = rawItem.Count; - const damageValue = rawItem.Damage; - const itemTag = rawItem.tag; - const extraAttributes = itemTag?.ExtraAttributes ?? {}; - let headId; - if (vanillaId === 397) { - const headDataBase64 = itemTag?.SkullOwner?.Properties?.textures?.[0]?.Value; - if (headDataBase64) { - const headData = JSON.parse(base64decode(headDataBase64).toString()); - const headDataUrl = headData?.textures?.SKIN?.url; - if (headDataUrl) { - const splitUrl = headDataUrl.split('/'); - headId = splitUrl[splitUrl.length - 1]; - } - } - } - return { - id: extraAttributes?.id ?? null, - count: itemCount ?? 1, - vanillaId: damageValue ? `${vanillaId}:${damageValue}` : vanillaId.toString(), - display: { - name: itemTag?.display?.Name ?? 'null', - lore: itemTag?.display?.Lore ?? [], - // if it has an ench value in the tag, then it should have an enchant glint effect - glint: (itemTag?.ench ?? []).length > 0 - }, - reforge: extraAttributes?.modifier, - enchantments: extraAttributes?.enchantments, - anvil_uses: extraAttributes?.anvil_uses, - timestamp: extraAttributes?.timestamp, - head_texture: headId, - }; -} -function cleanItems(rawItems) { - return rawItems.map(cleanItem); -} -export function cleanInventory(encodedNbt) { - return new Promise(resolve => { - const base64Data = base64decode(encodedNbt); - nbt.parse(base64Data, false, (err, value) => { - const simplifiedNbt = nbt.simplify(value); - // do some basic cleaning on the items and return - resolve(cleanItems(simplifiedNbt.i)); - }); - }); -} -export const INVENTORIES = { - armor: 'inv_armor', - inventory: 'inv_contents', - ender_chest: 'ender_chest_contents', - talisman_bag: 'talisman_bag', - potion_bag: 'potion_bag', - fishing_bag: 'fishing_bag', - quiver: 'quiver', - trick_or_treat_bag: 'candy_inventory_contents', - wardrobe: 'wardrobe_contents' -}; -export async function cleanInventories(data) { - const cleanInventories = {}; - for (const cleanInventoryName in INVENTORIES) { - const hypixelInventoryName = INVENTORIES[cleanInventoryName]; - const encodedInventoryContents = data[hypixelInventoryName]?.data; - let inventoryContents; - if (encodedInventoryContents) { - inventoryContents = await cleanInventory(encodedInventoryContents); - if (cleanInventoryName === 'armor') - // the armor is sent from boots to head, the opposite makes more sense - inventoryContents.reverse(); - cleanInventories[cleanInventoryName] = inventoryContents; - } - } - return cleanInventories; -} diff --git a/build/cleaners/skyblock/itemId.js b/build/cleaners/skyblock/itemId.js deleted file mode 100644 index ea94771..0000000 --- a/build/cleaners/skyblock/itemId.js +++ /dev/null @@ -1,57 +0,0 @@ -// change weird item names to be more consistent with vanilla -const ITEMS = { - 'log': 'oak_log', - 'log:1': 'spruce_log', - 'log:2': 'birch_log', - 'log:3': 'jungle_log', - 'log_2': 'acacia_log', - 'log_2:1': 'dark_oak_log', - 'ink_sack': 'ink_sac', - 'ink_sack:3': 'cocoa_beans', - 'ink_sack:4': 'lapis_lazuli', - 'cocoa': 'cocoa_beans', - 'raw_fish': 'cod', - 'raw_fish:1': 'salmon', - 'raw_fish:2': 'tropical_fish', - 'raw_fish:3': 'pufferfish', - 'raw_salmon': 'salmon', - 'cooked_fish': 'cooked_cod', - 'seeds': 'wheat_seeds', - 'sulphur': 'gunpowder', - 'raw_chicken': 'chicken', - 'pork': 'porkchop', - 'potato_item': 'potato', - 'carrot_item': 'carrot', - 'mushroom_collection': 'red_mushroom', - 'nether_stalk': 'nether_wart', - 'water_lily': 'lily_pad', - 'melon': 'melon_slice', - 'ender_stone': 'end_stone', - 'huge_mushroom_1': 'red_mushroom_block', - 'huge_mushroom_2': 'brown_mushroom_block', - 'iron_ingot': 'iron_ingot', - 'iron': 'iron_ingot', - 'gold': 'gold_ingot', - 'endstone': 'end_stone', - 'lapis_lazuli_block': 'lapis_block', - 'snow_ball': 'snowball', - 'raw_beef': 'beef', - 'eye_of_ender': 'ender_eye', - 'grilled_pork': 'cooked_porkchop', - 'glistering_melon': 'glistering_melon_slice', - 'cactus_green': 'green_dye', - 'enchanted_lapis_lazuli': 'enchanted_lapis_lazuli', - 'enchanted_potato': 'enchanted_potato', - 'enchanted_birch_log': 'enchanted_birch_log', - 'enchanted_gunpowder': 'enchanted_gunpowder', - 'enchanted_raw_salmon': 'enchanted_salmon', - 'enchanted_raw_chicken': 'enchanted_chicken', - 'enchanted_water_lily': 'enchanted_lily_pad', - 'enchanted_ink_sack': 'enchanted_ink_sac', - 'enchanted_melon': 'enchanted_melon_slice', - 'enchanted_glistering_melon': 'enchanted_glistering_melon_slice' -}; -/** Clean an item with a weird name (log_2:1) and make it have a better name (dark_oak_log) */ -export function cleanItemId(itemId) { - return ITEMS[itemId.toLowerCase()] ?? itemId.toLowerCase(); -} diff --git a/build/cleaners/skyblock/member.js b/build/cleaners/skyblock/member.js deleted file mode 100644 index 3a6a143..0000000 --- a/build/cleaners/skyblock/member.js +++ /dev/null @@ -1,54 +0,0 @@ -import { cleanCollections } from './collections.js'; -import { cleanInventories } from './inventory.js'; -import { cleanFairySouls } from './fairysouls.js'; -import { cleanObjectives } from './objectives.js'; -import { cleanProfileStats } from './stats.js'; -import { cleanMinions } from './minions.js'; -import { cleanSlayers } from './slayers.js'; -import { cleanVisitedZones } from './zones.js'; -import { cleanSkills } from './skills.js'; -import * as cached from '../../hypixelCached.js'; -import * as constants from '../../constants.js'; -export async function cleanSkyBlockProfileMemberResponseBasic(member) { - const player = await cached.fetchPlayer(member.uuid); - if (!player) - return null; - return { - uuid: member.uuid, - username: player.username, - last_save: member.last_save / 1000, - first_join: member.first_join / 1000, - rank: player.rank - }; -} -/** Cleans up a member (from skyblock/profile) */ -export async function cleanSkyBlockProfileMemberResponse(member, included = undefined) { - // profiles.members[] - const inventoriesIncluded = included === undefined || included.includes('inventories'); - const player = await cached.fetchPlayer(member.uuid); - if (!player) - return null; - const fairySouls = cleanFairySouls(member); - const { max_fairy_souls: maxFairySouls } = await constants.fetchConstantValues(); - if (fairySouls.total > (maxFairySouls ?? 0)) - await constants.setConstantValues({ max_fairy_souls: fairySouls.total }); - return { - uuid: member.uuid, - username: player.username, - last_save: member.last_save / 1000, - first_join: member.first_join / 1000, - rank: player.rank, - purse: member.coin_purse, - stats: cleanProfileStats(member), - // this is used for leaderboards - rawHypixelStats: member.stats ?? {}, - minions: await cleanMinions(member), - fairy_souls: fairySouls, - inventories: inventoriesIncluded ? await cleanInventories(member) : undefined, - objectives: cleanObjectives(member), - skills: await cleanSkills(member), - visited_zones: await cleanVisitedZones(member), - collections: cleanCollections(member), - slayers: cleanSlayers(member) - }; -} diff --git a/build/cleaners/skyblock/minions.js b/build/cleaners/skyblock/minions.js deleted file mode 100644 index 5c0bd9a..0000000 --- a/build/cleaners/skyblock/minions.js +++ /dev/null @@ -1,85 +0,0 @@ -import { maxMinion } from '../../hypixel.js'; -import * as constants from '../../constants.js'; -/** - * Clean the minions provided by Hypixel - * @param minionsRaw The minion data provided by the Hypixel API - */ -export async function cleanMinions(member) { - const minions = []; - const processedMinionNames = new Set(); - for (const minionRaw of member?.crafted_generators ?? []) { - // do some regex magic to get the minion name and level - // examples of potential minion names: CLAY_11, PIG_1, MAGMA_CUBE_4 - const minionName = minionRaw.split(/_\d/)[0].toLowerCase(); - const minionLevel = parseInt(minionRaw.split(/\D*_/)[1]); - let matchingMinion = minions.find(m => m.name === minionName); - if (!matchingMinion) { - // if the minion doesnt already exist in the minions array, then create it - matchingMinion = { - name: minionName, - levels: new Array(maxMinion).fill(false) - }; - minions.push(matchingMinion); - } - while (minionLevel > matchingMinion.levels.length) - // if hypixel increases the minion level, this will increase with it - matchingMinion.levels.push(false); - // set the minion at that level to true - matchingMinion.levels[minionLevel - 1] = true; - processedMinionNames.add(minionName); - } - const allMinionNames = new Set(await constants.fetchMinions()); - for (const minionName of processedMinionNames) { - if (!allMinionNames.has(minionName)) { - constants.addMinions(Array.from(processedMinionNames)); - break; - } - } - for (const minionName of allMinionNames) { - if (!processedMinionNames.has(minionName)) { - processedMinionNames.add(minionName); - minions.push({ - name: minionName, - levels: new Array(maxMinion).fill(false) - }); - } - } - return minions.sort((a, b) => a.name > b.name ? 1 : (a.name < b.name ? -1 : 0)); -} -/** - * Combine multiple arrays of minions into one, useful when getting the minions for members - * @param minions An array of arrays of minions - */ -export function combineMinionArrays(minions) { - const resultMinions = []; - for (const memberMinions of minions) { - for (const minion of memberMinions) { - // this is a reference, so we can directly modify the attributes for matchingMinionReference - // and they'll be in the resultMinions array - const matchingMinionReference = resultMinions.find(m => m.name === minion.name); - if (!matchingMinionReference) { - // if the minion name isn't already in the array, add it! - resultMinions.push(minion); - } - else { - // This should never happen, but in case the length of `minion.levels` is longer than - // `matchingMinionReference.levels`, then it should be extended to be equal length - while (matchingMinionReference.levels.length < minion.levels.length) - matchingMinionReference.levels.push(false); - for (let i = 0; i < minion.levels.length; i++) { - if (minion.levels[i]) - matchingMinionReference.levels[i] = true; - } - } - } - } - return resultMinions; -} -export function countUniqueMinions(minions) { - let uniqueMinions = 0; - for (const minion of minions) { - // find the number of times `true` is in the list and add it to uniqueMinions - uniqueMinions += minion.levels.filter(x => x).length; - } - return uniqueMinions; -} diff --git a/build/cleaners/skyblock/objectives.js b/build/cleaners/skyblock/objectives.js deleted file mode 100644 index 52e92db..0000000 --- a/build/cleaners/skyblock/objectives.js +++ /dev/null @@ -1,12 +0,0 @@ -export function cleanObjectives(data) { - const rawObjectives = data?.objectives || {}; - const objectives = []; - for (const rawObjectiveName in rawObjectives) { - const rawObjectiveValue = rawObjectives[rawObjectiveName]; - objectives.push({ - name: rawObjectiveName, - completed: rawObjectiveValue.status === 'COMPLETE', - }); - } - return objectives; -} diff --git a/build/cleaners/skyblock/profile.js b/build/cleaners/skyblock/profile.js deleted file mode 100644 index 42e26b3..0000000 --- a/build/cleaners/skyblock/profile.js +++ /dev/null @@ -1,64 +0,0 @@ -import { cleanSkyBlockProfileMemberResponse, cleanSkyBlockProfileMemberResponseBasic } from './member.js'; -import { combineMinionArrays, countUniqueMinions } from './minions.js'; -import * as constants from '../../constants.js'; -import { cleanBank } from './bank.js'; -/** Return a `CleanProfile` instead of a `CleanFullProfile`, useful when we need to get members but don't want to waste much ram */ -export async function cleanSkyblockProfileResponseLighter(data) { - // We use Promise.all so it can fetch all the usernames at once instead of waiting for the previous promise to complete - const promises = []; - 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(cleanSkyBlockProfileMemberResponseBasic(memberRaw)); - } - const cleanedMembers = (await Promise.all(promises)).filter(m => m); - return { - uuid: data.profile_id, - name: data.cute_name, - members: cleanedMembers, - }; -} -/** - * 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, options) { - // We use Promise.all so it can fetch all the users at once instead of waiting for the previous promise to complete - const promises = []; - if (!data) - return null; - for (const memberUUID in data.members) { - const memberRaw = data.members[memberUUID]; - memberRaw.uuid = memberUUID; - promises.push(cleanSkyBlockProfileMemberResponse(memberRaw, [ - !options?.basic ? 'stats' : undefined, - options?.mainMemberUuid === memberUUID ? 'inventories' : undefined - ])); - } - const cleanedMembers = (await Promise.all(promises)).filter(m => m !== null && m !== undefined); - if (options?.basic) { - return { - uuid: data.profile_id, - name: data.cute_name, - members: cleanedMembers, - }; - } - const memberMinions = []; - for (const member of cleanedMembers) { - memberMinions.push(member.minions); - } - const minions = combineMinionArrays(memberMinions); - const { max_minions: maxUniqueMinions } = await constants.fetchConstantValues(); - const uniqueMinions = countUniqueMinions(minions); - if (uniqueMinions > (maxUniqueMinions ?? 0)) - await constants.setConstantValues({ max_minions: uniqueMinions }); - // return more detailed info - return { - uuid: data.profile_id, - name: data.cute_name, - members: cleanedMembers, - bank: cleanBank(data), - minions: minions, - minion_count: uniqueMinions - }; -} diff --git a/build/cleaners/skyblock/profiles.js b/build/cleaners/skyblock/profiles.js deleted file mode 100644 index a85b1f3..0000000 --- a/build/cleaners/skyblock/profiles.js +++ /dev/null @@ -1,21 +0,0 @@ -import { cleanSkyblockProfileResponse } from './profile.js'; -export function cleanPlayerSkyblockProfiles(rawProfiles) { - let profiles = []; - for (const profile of Object.values(rawProfiles ?? {})) { - profiles.push({ - uuid: profile.profile_id, - name: profile.cute_name - }); - } - return profiles; -} -/** Convert an array of raw profiles into clean profiles */ -export async function cleanSkyblockProfilesResponse(data) { - const promises = []; - for (const profile of data ?? []) { - // let cleanedProfile = await cleanSkyblockProfileResponseLighter(profile) - promises.push(cleanSkyblockProfileResponse(profile)); - } - const cleanedProfiles = (await Promise.all(promises)).filter(p => p); - return cleanedProfiles; -} diff --git a/build/cleaners/skyblock/skills.js b/build/cleaners/skyblock/skills.js deleted file mode 100644 index e9b19f8..0000000 --- a/build/cleaners/skyblock/skills.js +++ /dev/null @@ -1,146 +0,0 @@ -// the highest level you can have in each skill -// numbers taken from https://hypixel-skyblock.fandom.com/wiki/Skills -const skillsMaxLevel = { - farming: 60, - mining: 60, - combat: 60, - foraging: 50, - fishing: 50, - enchanting: 60, - alchemy: 50, - taming: 50, - dungeoneering: 50, - carpentry: 50, - runecrafting: 25, - social: 25 -}; -const skillXpTable = [ - 50, - 175, - 375, - 675, - 1175, - 1925, - 2925, - 4425, - 6425, - 9925, - 14925, - 22425, - 32425, - 47425, - 67425, - 97425, - 147425, - 222425, - 322425, - 522425, - 822425, - 1222425, - 1722425, - 2322425, - 3022425, - 3822425, - 4722425, - 5722425, - 6822425, - 8022425, - 9322425, - 10722425, - 12222425, - 13822425, - 15522425, - 17322425, - 19222425, - 21222425, - 23322425, - 25522425, - 27822425, - 30222425, - 32722425, - 35322425, - 38072425, - 40972425, - 44072425, - 47472425, - 51172425, - 55172425, - 59472425, - 64072425, - 68972425, - 74172425, - 79672425, - 85472425, - 91572425, - 97972425, - 104672425, - 111672425 // 60 -]; -const skillXpTableEasier = [ - 50, - 150, - 275, - 435, - 635, - 885, - 1200, - 1600, - 2100, - 2725, - 3510, - 4510, - 5760, - 7325, - 9325, - 11825, - 14950, - 18950, - 23950, - 30200, - 38050, - 47850, - 60100, - 75400, - 94450 // 25 -]; -// for skills that aren't in maxSkills, default to this -const skillsDefaultMaxLevel = 50; -/** - * Get the skill level for the amount of total xp - * @param xp The xp we're finding the level for - * @param easierLevel Whether it should use the alternate leveling xp table (used for cosmetic skills and dungeoneering) - */ -export function levelForSkillXp(xp, maxLevel) { - const xpTable = (maxLevel <= 25 ? skillXpTableEasier : skillXpTable).slice(0, maxLevel); - const skillLevel = [...xpTable].reverse().findIndex(levelXp => xp >= levelXp); - return skillLevel === -1 ? 0 : xpTable.length - skillLevel; -} -export async function cleanSkills(data) { - const skills = []; - for (const item in data) { - if (item.startsWith('experience_skill_')) { - const skillName = item.substr('experience_skill_'.length); - // the amount of total xp you have in this skill - const skillXp = data[item]; - const skillMaxLevel = skillsMaxLevel[skillName] ?? skillsDefaultMaxLevel; - const xpTable = (skillMaxLevel <= 25 ? skillXpTableEasier : skillXpTable).slice(0, skillMaxLevel); - // the level you're at for this skill - const skillLevel = levelForSkillXp(skillXp, skillMaxLevel); - // the total xp required for the previous level - const previousLevelXp = skillLevel >= 1 ? xpTable[skillLevel - 1] : 0; - // the extra xp left over - const skillLevelXp = skillXp - previousLevelXp; - // the amount of extra xp required for this level - const skillLevelXpRequired = xpTable[skillLevel] - previousLevelXp; - skills.push({ - name: skillName, - xp: skillXp, - level: skillLevel, - maxLevel: skillMaxLevel, - levelXp: skillLevelXp, - levelXpRequired: skillLevelXpRequired - }); - } - } - return skills; -} diff --git a/build/cleaners/skyblock/slayers.js b/build/cleaners/skyblock/slayers.js deleted file mode 100644 index 75894f7..0000000 --- a/build/cleaners/skyblock/slayers.js +++ /dev/null @@ -1,60 +0,0 @@ -export const slayerLevels = 5; -const SLAYER_NAMES = { - spider: 'tarantula', - zombie: 'revenant', - wolf: 'sven' -}; -export function cleanSlayers(data) { - const slayers = []; - const slayersDataRaw = data?.slayer_bosses; - let totalXp = 0; - let totalKills = 0; - for (const slayerNameRaw in slayersDataRaw) { - const slayerDataRaw = slayersDataRaw[slayerNameRaw]; - // convert name provided by api (spider) to the real name (tarantula) - const slayerName = SLAYER_NAMES[slayerNameRaw]; - const slayerXp = slayerDataRaw.xp ?? 0; - let slayerKills = 0; - const slayerTiers = []; - for (const slayerDataKey in slayerDataRaw) { - // if a key starts with boss_kills_tier_ (boss_kills_tier_1), get the last number - if (slayerDataKey.startsWith('boss_kills_tier_')) { - const slayerTierRaw = parseInt(slayerDataKey.substr('boss_kills_tier_'.length)); - const slayerTierKills = slayerDataRaw[slayerDataKey] ?? 0; - // add 1 since hypixel is using 0 indexed tiers - const slayerTier = slayerTierRaw + 1; - slayerTiers.push({ - kills: slayerTierKills, - tier: slayerTier - }); - // count up the total number of kills for this slayer - if (slayerTierKills) - slayerKills += slayerTierKills; - } - } - // if the slayer tier length is less than the max, add more empty ones - while (slayerTiers.length < slayerLevels) - slayerTiers.push({ - tier: slayerTiers.length + 1, - kills: 0 - }); - const slayer = { - name: slayerName, - raw_name: slayerNameRaw, - tiers: slayerTiers, - xp: slayerXp ?? 0, - kills: slayerKills - }; - slayers.push(slayer); - // add the xp and kills from this slayer to the total xp - if (slayerXp) - totalXp += slayerXp; - if (slayerKills) - totalKills += slayerKills; - } - return { - xp: totalXp, - kills: totalKills, - bosses: slayers - }; -} diff --git a/build/cleaners/skyblock/stats.js b/build/cleaners/skyblock/stats.js deleted file mode 100644 index 361f1ba..0000000 --- a/build/cleaners/skyblock/stats.js +++ /dev/null @@ -1,91 +0,0 @@ -const statCategories = { - 'deaths': ['deaths_', 'deaths'], - 'kills': ['kills_', 'kills'], - 'fishing': ['items_fished_', 'items_fished', 'shredder_'], - 'auctions': ['auctions_'], - 'races': ['_best_time', '_best_time_2'], - 'mythos': ['mythos_burrows_', 'mythos_kills'], - 'collection': ['collection_'], - 'skills': ['skill_'], - 'slayer': ['slayer_'], - 'misc': null // everything else goes here -}; -export function categorizeStat(statNameRaw) { - // 'deaths_void' - for (const statCategory in statCategories) { - // 'deaths' - const statCategoryMatchers = statCategories[statCategory]; - if (statCategoryMatchers == null) { - // If it's null, just go with this. Can only ever be 'misc' - return { - category: statCategory, - name: statNameRaw - }; - } - for (const categoryMatch of statCategoryMatchers) { - // ['deaths_'] - let trailingEnd = categoryMatch[0] === '_'; - let trailingStart = categoryMatch.substr(-1) === '_'; - if (trailingStart && statNameRaw.startsWith(categoryMatch)) { - return { - category: statCategory, - name: statNameRaw.substr(categoryMatch.length) - }; - } - else if (trailingEnd && statNameRaw.endsWith(categoryMatch)) { - return { - category: statCategory, - name: statNameRaw.substr(0, statNameRaw.length - categoryMatch.length) - }; - } - else if (statNameRaw == categoryMatch) { - // if it matches exactly, we don't know the name. will be defaulted to category later on - return { - category: statCategory, - name: null - }; - } - } - } - // this should never happen, as it'll default to misc and return if nothing is found - return { - category: null, - name: statNameRaw - }; -} -export const statUnits = { - time: ['_best_time', '_best_time_2'], - date: ['first_join'], - coins: ['purse'], - leaderboards: ['leaderboards_count', 'top_1_leaderboards_count'] -}; -export function getStatUnit(name) { - for (const [unitName, statMatchers] of Object.entries(statUnits)) { - for (const statMatch of statMatchers) { - let trailingEnd = statMatch[0] === '_'; - let trailingStart = statMatch.substr(-1) === '_'; - if ((trailingStart && name.startsWith(statMatch)) - || (trailingEnd && name.endsWith(statMatch)) - || (name == statMatch)) - return unitName; - } - } - return null; -} -export function cleanProfileStats(data) { - // TODO: add type for statsRaw (probably in hypixelApi.ts since its coming from there) - const stats = []; - const rawStats = data?.stats ?? {}; - for (const statNameRaw in rawStats) { - const statValue = rawStats[statNameRaw]; - let { category: statCategory, name: statName } = categorizeStat(statNameRaw); - stats.push({ - categorizedName: statName ?? 'total', - value: statValue, - rawName: statNameRaw, - category: statCategory, - unit: getStatUnit(statNameRaw) ?? null - }); - } - return stats; -} diff --git a/build/cleaners/skyblock/zones.js b/build/cleaners/skyblock/zones.js deleted file mode 100644 index 90c689b..0000000 --- a/build/cleaners/skyblock/zones.js +++ /dev/null @@ -1,23 +0,0 @@ -import * as constants from '../../constants.js'; -export async function cleanVisitedZones(data) { - const rawZones = data?.visited_zones || []; - // TODO: store all the zones that exist in SkyBlock, add add those to the array with visited being false - const zones = []; - const knownZones = await constants.fetchZones(); - for (const rawZoneName of knownZones) { - zones.push({ - name: rawZoneName, - visited: rawZones.includes(rawZoneName) - }); - } - // if this user somehow has a zone that we don't know about, just add it to zones - for (const rawZoneName of rawZones) { - if (!knownZones.includes(rawZoneName)) { - zones.push({ - name: rawZoneName, - visited: true - }); - } - } - return zones; -} diff --git a/build/cleaners/socialmedia.js b/build/cleaners/socialmedia.js deleted file mode 100644 index c901725..0000000 --- a/build/cleaners/socialmedia.js +++ /dev/null @@ -1,6 +0,0 @@ -export function cleanSocialMedia(data) { - return { - discord: data?.socialMedia?.links?.DISCORD || null, - forums: data?.socialMedia?.links?.HYPIXEL || null - }; -} diff --git a/build/constants.js b/build/constants.js deleted file mode 100644 index 7dcb210..0000000 --- a/build/constants.js +++ /dev/null @@ -1,226 +0,0 @@ -/** - * Fetch and edit constants from the skyblock-constants repo - */ -// we have to do this so we can mock the function from the tests properly -import * as constants from './constants.js'; -import NodeCache from 'node-cache'; -import { debug } from './index.js'; -import Queue from 'queue-promise'; -import fetch from 'node-fetch'; -import { Agent } from 'https'; -const httpsAgent = new Agent({ - keepAlive: true -}); -const githubApiBase = 'https://api.github.com'; -const owner = 'skyblockstats'; -const repo = 'skyblock-constants'; -// we use a queue for editing so it always utilizes the cache if possible, and to avoid hitting the github rateimit -const queue = new Queue({ - concurrent: 1, - interval: 10 -}); -/** - * Send a request to the GitHub API - * @param method The HTTP method, for example GET, PUT, POST, etc - * @param route The route to send the request to - * @param headers The extra headers - * @param json The JSON body, only applicable for some types of methods - */ -async function fetchGithubApi(method, route, headers, json) { - try { - if (debug) - console.debug('fetching github api', method, route); - const data = await fetch(githubApiBase + route, { - agent: () => httpsAgent, - body: json ? JSON.stringify(json) : undefined, - method, - headers: Object.assign({ - 'Authorization': `token ${process.env.github_token}` - }, headers), - }); - if (debug) - console.debug('fetched github api', method, route); - return data; - } - catch { - // if there's an error, wait a second and try again - await new Promise((resolve) => setTimeout(resolve, 1000)); - return await fetchGithubApi(method, route, headers, json); - } -} -// cache files for an hour -const fileCache = new NodeCache({ - stdTTL: 60 * 60, - checkperiod: 60, - useClones: false, -}); -/** - * Fetch a file from skyblock-constants - * @param path The file path, for example stats.json - */ -function fetchFile(path) { - return new Promise(resolve => { - queue.enqueue(async () => { - if (fileCache.has(path)) - return resolve(fileCache.get(path)); - const r = await fetchGithubApi('GET', `/repos/${owner}/${repo}/contents/${path}`, { - 'Accept': 'application/vnd.github.v3+json', - }); - const data = await r.json(); - const file = { - path: data.path, - content: Buffer.from(data.content, data.encoding).toString(), - sha: data.sha - }; - fileCache.set(path, file); - resolve(file); - }); - }); -} -/** - * Edit a file on skyblock-constants - * @param file The GithubFile you got from fetchFile - * @param message The commit message - * @param newContent The new content in the file - */ -async function editFile(file, message, newContent) { - const r = await fetchGithubApi('PUT', `/repos/${owner}/${repo}/contents/${file.path}`, { 'Content-Type': 'application/json' }, { - message: message, - content: Buffer.from(newContent).toString('base64'), - sha: file.sha, - branch: 'main' - }); - const data = await r.json(); - fileCache.set(file.path, { - path: data.content.path, - content: newContent, - sha: data.content.sha - }); -} -export let fetchJSONConstant = async function fetchJSONConstant(filename) { - const file = await fetchFile(filename); - try { - return JSON.parse(file.content); - } - catch { - // probably invalid json, return an empty array - return []; - } -}; -/** Add stats to skyblock-constants. This has caching so it's fine to call many times */ -export let addJSONConstants = async function addJSONConstants(filename, addingValues, unit = 'stat') { - if (addingValues.length === 0) - return; // no stats provided, just return - let file = await fetchFile(filename); - if (!file.path) - return; - let oldStats; - try { - oldStats = JSON.parse(file.content); - } - catch { - // invalid json, set it as an empty array - oldStats = []; - } - const updatedStats = oldStats - .concat(addingValues) - // remove duplicates - .filter((value, index, array) => array.indexOf(value) === index) - .sort((a, b) => a.localeCompare(b)); - const newStats = updatedStats.filter(value => !oldStats.includes(value)); - // there's not actually any new stats, just return - if (newStats.length === 0) - return; - const commitMessage = newStats.length >= 2 ? `Add ${newStats.length} new ${unit}s` : `Add '${newStats[0]}' ${unit}`; - try { - await editFile(file, commitMessage, JSON.stringify(updatedStats, null, 2)); - } - catch { - // the file probably changed or something, try again - file = await fetchFile(filename); - await editFile(file, commitMessage, JSON.stringify(updatedStats, null, 2)); - } -}; -/** Fetch all the known SkyBlock stats as an array of strings */ -export async function fetchStats() { - return await constants.fetchJSONConstant('stats.json'); -} -/** Add stats to skyblock-constants. This has caching so it's fine to call many times */ -export async function addStats(addingStats) { - await constants.addJSONConstants('stats.json', addingStats, 'stat'); -} -/** Fetch all the known SkyBlock collections as an array of strings */ -export async function fetchCollections() { - return await constants.fetchJSONConstant('collections.json'); -} -/** Add collections to skyblock-constants. This has caching so it's fine to call many times */ -export async function addCollections(addingCollections) { - await constants.addJSONConstants('collections.json', addingCollections, 'collection'); -} -/** Fetch all the known SkyBlock collections as an array of strings */ -export async function fetchSkills() { - return await constants.fetchJSONConstant('skills.json'); -} -/** Add skills to skyblock-constants. This has caching so it's fine to call many times */ -export async function addSkills(addingSkills) { - await constants.addJSONConstants('skills.json', addingSkills, 'skill'); -} -/** Fetch all the known SkyBlock collections as an array of strings */ -export async function fetchZones() { - return await constants.fetchJSONConstant('zones.json'); -} -/** Add skills to skyblock-constants. This has caching so it's fine to call many times */ -export async function addZones(addingZones) { - await constants.addJSONConstants('zones.json', addingZones, 'zone'); -} -/** Fetch all the known SkyBlock slayer names as an array of strings */ -export async function fetchSlayers() { - return await constants.fetchJSONConstant('slayers.json'); -} -/** Add skills to skyblock-constants. This has caching so it's fine to call many times */ -export async function addSlayers(addingSlayers) { - await constants.addJSONConstants('slayers.json', addingSlayers, 'slayer'); -} -/** Fetch all the known SkyBlock slayer names as an array of strings */ -export async function fetchMinions() { - return await constants.fetchJSONConstant('minions.json'); -} -export async function fetchSkillXp() { - return await constants.fetchJSONConstant('manual/skill_xp.json'); -} -export async function fetchSkillXpEasier() { - return await constants.fetchJSONConstant('manual/skill_xp_easier.json'); -} -/** Add skills to skyblock-constants. This has caching so it's fine to call many times */ -export async function addMinions(addingMinions) { - await constants.addJSONConstants('minions.json', addingMinions, 'minion'); -} -export async function fetchConstantValues() { - return await constants.fetchJSONConstant('values.json'); -} -export async function setConstantValues(newValues) { - let file = await fetchFile('values.json'); - if (!file.path) - return; - let oldValues; - try { - oldValues = JSON.parse(file.content); - } - catch { - // invalid json, set it as an empty array - oldValues = {}; - } - const updatedStats = { ...oldValues, ...newValues }; - // there's not actually any new stats, just return - // TODO: optimize this? might be fine already though, idk - if (JSON.stringify(updatedStats) === JSON.stringify(oldValues)) - return; - const commitMessage = 'Update values'; - try { - await editFile(file, commitMessage, JSON.stringify(updatedStats, null, 2)); - } - catch { } -} -// this is necessary for mocking in the tests because es6 -export function mockAddJSONConstants($value) { addJSONConstants = $value; } -export function mockFetchJSONConstant($value) { fetchJSONConstant = $value; } diff --git a/build/database.js b/build/database.js deleted file mode 100644 index cdad69f..0000000 --- a/build/database.js +++ /dev/null @@ -1,600 +0,0 @@ -/** - * Store data about members for leaderboards -*/ -import { categorizeStat, getStatUnit } from './cleaners/skyblock/stats.js'; -import { slayerLevels } from './cleaners/skyblock/slayers.js'; -import { MongoClient } from 'mongodb'; -import * as cached from './hypixelCached.js'; -import * as constants from './constants.js'; -import { shuffle, sleep } from './util.js'; -import NodeCache from 'node-cache'; -import { v4 as uuid4 } from 'uuid'; -import Queue from 'queue-promise'; -import { debug } from './index.js'; -// don't update the user for 3 minutes -const recentlyUpdated = new NodeCache({ - stdTTL: 60 * 3, - checkperiod: 60, - useClones: false, -}); -// don't add stuff to the queue within the same 5 minutes -const recentlyQueued = new NodeCache({ - stdTTL: 60 * 5, - checkperiod: 60, - useClones: false, -}); -export const cachedRawLeaderboards = new Map(); -const leaderboardMax = 100; -const reversedLeaderboards = [ - 'first_join', - '_best_time', '_best_time_2' -]; -let client; -let database; -let memberLeaderboardsCollection; -let profileLeaderboardsCollection; -let sessionsCollection; -let accountsCollection; -const leaderboardInfos = { - highest_crit_damage: 'This leaderboard is capped at the integer limit because Hypixel, look at the highest critical damage leaderboard instead.', - highest_critical_damage: 'uhhhhh yeah idk either', - leaderboards_count: 'This leaderboard counts how many leaderboards players are in the top 100 for.', - top_1_leaderboards_count: 'This leaderboard counts how many leaderboards players are #1 for.', - skill_social: 'This leaderboard is inaccurate because Hypixel only shows social skill data on some API profiles.' -}; -async function connect() { - if (!process.env.db_uri) - return console.warn('Warning: db_uri was not found in .env. Features that utilize the database such as leaderboards won\'t work.'); - if (!process.env.db_name) - return console.warn('Warning: db_name was not found in .env. Features that utilize the database such as leaderboards won\'t work.'); - client = await MongoClient.connect(process.env.db_uri); - database = client.db(process.env.db_name); - memberLeaderboardsCollection = database.collection('member-leaderboards'); - profileLeaderboardsCollection = database.collection('profile-leaderboards'); - sessionsCollection = database.collection('sessions'); - accountsCollection = database.collection('accounts'); - console.log('Connected to database :)'); -} -function getMemberCollectionAttributes(member) { - const collectionAttributes = {}; - for (const collection of member.collections) { - const collectionLeaderboardName = `collection_${collection.name}`; - collectionAttributes[collectionLeaderboardName] = collection.xp; - } - return collectionAttributes; -} -function getMemberSkillAttributes(member) { - const skillAttributes = {}; - for (const collection of member.skills) { - const skillLeaderboardName = `skill_${collection.name}`; - skillAttributes[skillLeaderboardName] = collection.xp; - } - return skillAttributes; -} -function getMemberSlayerAttributes(member) { - const slayerAttributes = { - slayer_total_xp: member.slayers.xp, - slayer_total_kills: member.slayers.kills, - }; - for (const slayer of member.slayers.bosses) { - slayerAttributes[`slayer_${slayer.raw_name}_total_xp`] = slayer.xp; - slayerAttributes[`slayer_${slayer.raw_name}_total_kills`] = slayer.kills; - for (const tier of slayer.tiers) { - slayerAttributes[`slayer_${slayer.raw_name}_${tier.tier}_kills`] = tier.kills; - } - } - return slayerAttributes; -} -function getMemberLeaderboardAttributes(member) { - // if you want to add a new leaderboard for member attributes, add it here (and getAllLeaderboardAttributes) - return { - // we use the raw stat names rather than the clean stats in case hypixel adds a new stat and it takes a while for us to clean it - ...member.rawHypixelStats, - // collection leaderboards - ...getMemberCollectionAttributes(member), - // skill leaderboards - ...getMemberSkillAttributes(member), - // slayer leaderboards - ...getMemberSlayerAttributes(member), - fairy_souls: member.fairy_souls.total, - first_join: member.first_join, - purse: member.purse, - visited_zones: member.visited_zones.length, - }; -} -function getProfileLeaderboardAttributes(profile) { - // if you want to add a new leaderboard for member attributes, add it here (and getAllLeaderboardAttributes) - return { - unique_minions: profile.minion_count - }; -} -export async function fetchAllLeaderboardsCategorized() { - const memberLeaderboardAttributes = await fetchAllMemberLeaderboardAttributes(); - const profileLeaderboardAttributes = await fetchAllProfileLeaderboardAttributes(); - const categorizedLeaderboards = {}; - for (const leaderboard of [...memberLeaderboardAttributes, ...profileLeaderboardAttributes]) { - const { category } = categorizeStat(leaderboard); - if (category) { - if (!categorizedLeaderboards[category]) - categorizedLeaderboards[category] = []; - categorizedLeaderboards[category].push(leaderboard); - } - } - // move misc to end by removing and readding it - const misc = categorizedLeaderboards.misc; - delete categorizedLeaderboards.misc; - categorizedLeaderboards.misc = misc; - return categorizedLeaderboards; -} -/** Fetch the raw names for the slayer leaderboards */ -export async function fetchSlayerLeaderboards() { - const rawSlayerNames = await constants.fetchSlayers(); - let leaderboardNames = [ - 'slayer_total_xp', - 'slayer_total_kills' - ]; - // we use the raw names (zombie, spider, wolf) instead of the clean names (revenant, tarantula, sven) because the raw names are guaranteed to never change - for (const slayerNameRaw of rawSlayerNames) { - leaderboardNames.push(`slayer_${slayerNameRaw}_total_xp`); - leaderboardNames.push(`slayer_${slayerNameRaw}_total_kills`); - for (let slayerTier = 1; slayerTier <= slayerLevels; slayerTier++) { - leaderboardNames.push(`slayer_${slayerNameRaw}_${slayerTier}_kills`); - } - } - return leaderboardNames; -} -/** Fetch the names of all the leaderboards that rank members */ -export async function fetchAllMemberLeaderboardAttributes() { - return [ - // we use the raw stat names rather than the clean stats in case hypixel adds a new stat and it takes a while for us to clean it - ...await constants.fetchStats(), - // collection leaderboards - ...(await constants.fetchCollections()).map(value => `collection_${value}`), - // skill leaderboards - ...(await constants.fetchSkills()).map(value => `skill_${value}`), - // slayer leaderboards - ...await fetchSlayerLeaderboards(), - 'fairy_souls', - 'first_join', - 'purse', - 'visited_zones', - 'leaderboards_count', - 'top_1_leaderboards_count' - ]; -} -/** Fetch the names of all the leaderboards that rank profiles */ -async function fetchAllProfileLeaderboardAttributes() { - return [ - 'unique_minions' - ]; -} -function isLeaderboardReversed(name) { - for (const leaderboardMatch of reversedLeaderboards) { - let trailingEnd = leaderboardMatch[0] === '_'; - let trailingStart = leaderboardMatch.substr(-1) === '_'; - if ((trailingStart && name.startsWith(leaderboardMatch)) - || (trailingEnd && name.endsWith(leaderboardMatch)) - || (name == leaderboardMatch)) - return true; - } - return false; -} -/** A set of names of the raw leaderboards that are currently being fetched. This is used to make sure two leaderboads aren't fetched at the same time */ -const fetchingRawLeaderboardNames = new Set(); -async function fetchMemberLeaderboardRaw(name) { - if (!client) - throw Error('Client isn\'t initialized yet'); - if (cachedRawLeaderboards.has(name)) - return cachedRawLeaderboards.get(name); - // if it's currently being fetched, check every 100ms until it's in cachedRawLeaderboards - if (fetchingRawLeaderboardNames.has(name)) { - while (true) { - await sleep(100); - if (cachedRawLeaderboards.has(name)) - return cachedRawLeaderboards.get(name); - } - } - // typescript forces us to make a new variable and set it this way because it gives an error otherwise - const query = {}; - query[`stats.${name}`] = { '$exists': true, '$ne': NaN }; - const sortQuery = {}; - sortQuery[`stats.${name}`] = isLeaderboardReversed(name) ? 1 : -1; - fetchingRawLeaderboardNames.add(name); - const leaderboardRaw = (await memberLeaderboardsCollection - .find(query) - .sort(sortQuery) - .limit(leaderboardMax) - .toArray()) - .map((i) => { - return { - profile: i.profile, - uuid: i.uuid, - value: i.stats[name] - }; - }); - fetchingRawLeaderboardNames.delete(name); - cachedRawLeaderboards.set(name, leaderboardRaw); - return leaderboardRaw; -} -async function fetchProfileLeaderboardRaw(name) { - if (cachedRawLeaderboards.has(name)) - return cachedRawLeaderboards.get(name); - // if it's currently being fetched, check every 100ms until it's in cachedRawLeaderboards - if (fetchingRawLeaderboardNames.has(name)) { - while (true) { - await sleep(100); - if (cachedRawLeaderboards.has(name)) - return cachedRawLeaderboards.get(name); - } - } - // typescript forces us to make a new variable and set it this way because it gives an error otherwise - const query = {}; - query[`stats.${name}`] = { '$exists': true, '$ne': NaN }; - const sortQuery = {}; - sortQuery[`stats.${name}`] = isLeaderboardReversed(name) ? 1 : -1; - fetchingRawLeaderboardNames.add(name); - const leaderboardRaw = (await profileLeaderboardsCollection - .find(query) - .sort(sortQuery) - .limit(leaderboardMax) - .toArray()) - .map((i) => { - return { - players: i.players, - uuid: i.uuid, - value: i.stats[name] - }; - }); - fetchingRawLeaderboardNames.delete(name); - cachedRawLeaderboards.set(name, leaderboardRaw); - return leaderboardRaw; -} -/** Fetch a leaderboard that ranks members, as opposed to profiles */ -export async function fetchMemberLeaderboard(name) { - const leaderboardRaw = await fetchMemberLeaderboardRaw(name); - const fetchLeaderboardPlayer = async (i) => { - const player = await cached.fetchBasicPlayer(i.uuid); - return { - player, - profileUuid: i.profile, - value: i.value - }; - }; - const promises = []; - for (const item of leaderboardRaw) { - promises.push(fetchLeaderboardPlayer(item)); - } - const leaderboard = await Promise.all(promises); - return { - name: name, - unit: getStatUnit(name) ?? null, - list: leaderboard - }; -} -/** Fetch a leaderboard that ranks profiles, as opposed to members */ -export async function fetchProfileLeaderboard(name) { - const leaderboardRaw = await fetchProfileLeaderboardRaw(name); - const fetchLeaderboardProfile = async (i) => { - const players = []; - for (const playerUuid of i.players) { - const player = await cached.fetchBasicPlayer(playerUuid); - if (player) - players.push(player); - } - return { - players: players, - profileUuid: i.uuid, - value: i.value - }; - }; - const promises = []; - for (const item of leaderboardRaw) { - promises.push(fetchLeaderboardProfile(item)); - } - const leaderboard = await Promise.all(promises); - return { - name: name, - unit: getStatUnit(name) ?? null, - list: leaderboard - }; -} -/** Fetch a leaderboard */ -export async function fetchLeaderboard(name) { - const profileLeaderboards = await fetchAllProfileLeaderboardAttributes(); - let leaderboard; - if (profileLeaderboards.includes(name)) { - leaderboard = await fetchProfileLeaderboard(name); - } - else { - leaderboard = await fetchMemberLeaderboard(name); - } - if (leaderboardInfos[name]) - leaderboard.info = leaderboardInfos[name]; - return leaderboard; -} -/** Get the leaderboard positions a member is on. This may take a while depending on whether stuff is cached */ -export async function fetchMemberLeaderboardSpots(player, profile) { - const fullProfile = await cached.fetchProfile(player, profile); - if (!fullProfile) - return null; - const fullMember = fullProfile.members.find(m => m.username.toLowerCase() === player.toLowerCase() || m.uuid === player); - if (!fullMember) - return null; - // update the leaderboard positions for the member - await updateDatabaseMember(fullMember, fullProfile); - const applicableAttributes = await getApplicableMemberLeaderboardAttributes(fullMember); - const memberLeaderboardSpots = []; - for (const leaderboardName in applicableAttributes) { - const leaderboard = await fetchMemberLeaderboardRaw(leaderboardName); - const leaderboardPositionIndex = leaderboard.findIndex(i => i.uuid === fullMember.uuid && i.profile === fullProfile.uuid); - memberLeaderboardSpots.push({ - name: leaderboardName, - positionIndex: leaderboardPositionIndex, - value: applicableAttributes[leaderboardName], - unit: getStatUnit(leaderboardName) ?? null - }); - } - return memberLeaderboardSpots; -} -async function getLeaderboardRequirement(name, leaderboardType) { - let leaderboard; - if (leaderboardType === 'member') - leaderboard = await fetchMemberLeaderboardRaw(name); - else if (leaderboardType === 'profile') - leaderboard = await fetchProfileLeaderboardRaw(name); - // if there's more than 100 items, return the 100th. if there's less, return null - return { - top_100: leaderboard[leaderboardMax - 1]?.value ?? null, - top_1: leaderboard[1]?.value ?? null - }; -} -/** Get the attributes for the member, but only ones that would put them on the top 100 for leaderboards */ -async function getApplicableMemberLeaderboardAttributes(member) { - const leaderboardAttributes = getMemberLeaderboardAttributes(member); - const applicableAttributes = {}; - const applicableTop1Attributes = {}; - for (const [leaderboard, attributeValue] of Object.entries(leaderboardAttributes)) { - const requirement = await getLeaderboardRequirement(leaderboard, 'member'); - const leaderboardReversed = isLeaderboardReversed(leaderboard); - if ((requirement.top_100 === null) - || (leaderboardReversed ? attributeValue < requirement.top_100 : attributeValue > requirement.top_100)) { - applicableAttributes[leaderboard] = attributeValue; - } - if ((requirement.top_1 === null) - || (leaderboardReversed ? attributeValue < requirement.top_1 : attributeValue > requirement.top_1)) { - applicableTop1Attributes[leaderboard] = attributeValue; - } - } - // add the "leaderboards count" attribute - const leaderboardsCount = Object.keys(applicableAttributes).length; - const leaderboardsCountRequirement = await getLeaderboardRequirement('leaderboards_count', 'member'); - if (leaderboardsCount > 0 - && ((leaderboardsCountRequirement