diff options
Diffstat (limited to 'build')
26 files changed, 0 insertions, 2769 deletions
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 <a href="/leaderboard/highest_critical_damage">highest critical damage leaderboard</a> 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.top_100 === null) - || (leaderboardsCount > leaderboardsCountRequirement.top_100))) - applicableAttributes['leaderboards_count'] = leaderboardsCount; - // add the "first leaderboards count" attribute - const top1LeaderboardsCount = Object.keys(applicableTop1Attributes).length; - const top1LeaderboardsCountRequirement = await getLeaderboardRequirement('top_1_leaderboards_count', 'member'); - if (top1LeaderboardsCount > 0 - && ((top1LeaderboardsCountRequirement.top_100 === null) - || (top1LeaderboardsCount > top1LeaderboardsCountRequirement.top_100))) - applicableAttributes['top_1_leaderboards_count'] = top1LeaderboardsCount; - return applicableAttributes; -} -/** Get the attributes for the profile, but only ones that would put them on the top 100 for leaderboards */ -async function getApplicableProfileLeaderboardAttributes(profile) { - const leaderboardAttributes = getProfileLeaderboardAttributes(profile); - const applicableAttributes = {}; - const applicableTop1Attributes = {}; - for (const [leaderboard, attributeValue] of Object.entries(leaderboardAttributes)) { - const requirement = await getLeaderboardRequirement(leaderboard, 'profile'); - const leaderboardReversed = isLeaderboardReversed(leaderboard); - if ((requirement.top_100 === null) - || (leaderboardReversed ? attributeValue < requirement.top_100 : attributeValue > requirement.top_100 - && attributeValue !== 0)) { - applicableAttributes[leaderboard] = attributeValue; - } - if ((requirement.top_1 === null) - || (leaderboardReversed ? attributeValue < requirement.top_1 : attributeValue > requirement.top_1 - && attributeValue !== 0)) { - applicableTop1Attributes[leaderboard] = attributeValue; - } - } - return applicableAttributes; -} -/** Update the member's leaderboard data on the server if applicable */ -export async function updateDatabaseMember(member, profile) { - if (!client) - return; // the db client hasn't been initialized - if (debug) - console.debug('updateDatabaseMember', member.username); - // the member's been updated too recently, just return - if (recentlyUpdated.get(profile.uuid + member.uuid)) - return; - // store the member in recentlyUpdated so it cant update for 3 more minutes - recentlyUpdated.set(profile.uuid + member.uuid, true); - if (debug) - console.debug('adding member to leaderboards', member.username); - if (member.rawHypixelStats) - await constants.addStats(Object.keys(member.rawHypixelStats)); - await constants.addCollections(member.collections.map(coll => coll.name)); - await constants.addSkills(member.skills.map(skill => skill.name)); - await constants.addZones(member.visited_zones.map(zone => zone.name)); - await constants.addSlayers(member.slayers.bosses.map(s => s.raw_name)); - if (debug) - console.debug('done constants..'); - const leaderboardAttributes = await getApplicableMemberLeaderboardAttributes(member); - if (debug) - console.debug('done getApplicableMemberLeaderboardAttributes..', leaderboardAttributes, member.username, profile.name); - await memberLeaderboardsCollection.updateOne({ - uuid: member.uuid, - profile: profile.uuid - }, { - '$set': { - stats: leaderboardAttributes, - last_updated: new Date() - } - }, { upsert: true }); - for (const [attributeName, attributeValue] of Object.entries(leaderboardAttributes)) { - const existingRawLeaderboard = await fetchMemberLeaderboardRaw(attributeName); - const leaderboardReverse = isLeaderboardReversed(attributeName); - const newRawLeaderboard = existingRawLeaderboard - // remove the player from the leaderboard, if they're there - .filter(value => value.uuid !== member.uuid || value.profile !== profile.uuid) - .concat([{ - value: attributeValue, - uuid: member.uuid, - profile: profile.uuid - }]) - .sort((a, b) => leaderboardReverse ? a.value - b.value : b.value - a.value) - .slice(0, 100); - cachedRawLeaderboards.set(attributeName, newRawLeaderboard); - } - if (debug) - console.debug('added member to leaderboards', member.username, leaderboardAttributes); -} -/** - * Update the profiles's leaderboard data on the server if applicable. - * This will not also update the members, you have to call updateDatabaseMember separately for that - */ -export async function updateDatabaseProfile(profile) { - if (!client) - return; // the db client hasn't been initialized - if (debug) - console.debug('updateDatabaseProfile', profile.name); - // the profile's been updated too recently, just return - if (recentlyUpdated.get(profile.uuid + 'profile')) - return; - // store the profile in recentlyUpdated so it cant update for 3 more minutes - recentlyUpdated.set(profile.uuid + 'profile', true); - if (debug) - console.debug('adding profile to leaderboards', profile.name); - const leaderboardAttributes = await getApplicableProfileLeaderboardAttributes(profile); - if (debug) - console.debug('done getApplicableProfileLeaderboardAttributes..', leaderboardAttributes, profile.name); - await profileLeaderboardsCollection.updateOne({ - uuid: profile.uuid - }, { - '$set': { - players: profile.members.map(p => p.uuid), - stats: leaderboardAttributes, - last_updated: new Date() - } - }, { upsert: true }); - // add the profile to the cached leaderboard without having to refetch it - for (const [attributeName, attributeValue] of Object.entries(leaderboardAttributes)) { - const existingRawLeaderboard = await fetchProfileLeaderboardRaw(attributeName); - const leaderboardReverse = isLeaderboardReversed(attributeName); - const newRawLeaderboard = existingRawLeaderboard - // remove the player from the leaderboard, if they're there - .filter(value => value.uuid !== profile.uuid) - .concat([{ - value: attributeValue, - uuid: profile.uuid, - players: profile.members.map(p => p.uuid) - }]) - .sort((a, b) => leaderboardReverse ? a.value - b.value : b.value - a.value) - .slice(0, 100); - cachedRawLeaderboards.set(attributeName, newRawLeaderboard); - } - if (debug) - console.debug('added profile to leaderboards', profile.name, leaderboardAttributes); -} -export const leaderboardUpdateMemberQueue = new Queue({ - concurrent: 1, - interval: 50 -}); -export const leaderboardUpdateProfileQueue = new Queue({ - concurrent: 1, - interval: 500 -}); -/** Queue an update for the member's leaderboard data on the server if applicable */ -export function queueUpdateDatabaseMember(member, profile) { - if (recentlyQueued.get(profile.uuid + member.uuid)) - return; - else - recentlyQueued.set(profile.uuid + member.uuid, true); - leaderboardUpdateMemberQueue.enqueue(async () => await updateDatabaseMember(member, profile)); -} -/** Queue an update for the profile's leaderboard data on the server if applicable */ -export function queueUpdateDatabaseProfile(profile) { - if (recentlyQueued.get(profile.uuid + 'profile')) - return; - else - recentlyQueued.set(profile.uuid + 'profile', true); - leaderboardUpdateProfileQueue.enqueue(async () => await updateDatabaseProfile(profile)); -} -/** - * Remove leaderboard attributes for members that wouldn't actually be on the leaderboard. This saves a lot of storage space - */ -async function removeBadMemberLeaderboardAttributes() { - const leaderboards = await fetchAllMemberLeaderboardAttributes(); - // shuffle so if the application is restarting many times itll still be useful - for (const leaderboard of shuffle(leaderboards)) { - // wait 10 seconds so it doesnt use as much ram - await sleep(10 * 1000); - const unsetValue = {}; - unsetValue[leaderboard] = ''; - const filter = {}; - const requirement = await getLeaderboardRequirement(leaderboard, 'member'); - const leaderboardReversed = isLeaderboardReversed(leaderboard); - if (requirement !== null) { - filter[`stats.${leaderboard}`] = { - '$lt': leaderboardReversed ? undefined : requirement, - '$gt': leaderboardReversed ? requirement : undefined - }; - await memberLeaderboardsCollection.updateMany(filter, { '$unset': unsetValue }); - } - } - await memberLeaderboardsCollection.deleteMany({ stats: {} }); - await profileLeaderboardsCollection.deleteMany({ stats: {} }); -} -export let finishedCachingRawLeaderboards = false; -/** Fetch all the leaderboards, used for caching. Don't call this often! */ -async function fetchAllLeaderboards(fast) { - const leaderboards = await fetchAllMemberLeaderboardAttributes(); - if (debug) - console.debug('Caching raw leaderboards!'); - for (const leaderboard of shuffle(leaderboards)) - await fetchMemberLeaderboardRaw(leaderboard); - finishedCachingRawLeaderboards = true; -} -export async function createSession(refreshToken, userData) { - const sessionId = uuid4(); - await sessionsCollection?.insertOne({ - _id: sessionId, - refresh_token: refreshToken, - discord_user: { - id: userData.id, - name: userData.username + '#' + userData.discriminator - }, - lastUpdated: new Date() - }); - return sessionId; -} -export async function fetchSession(sessionId) { - return await sessionsCollection?.findOne({ _id: sessionId }); -} -export async function fetchAccount(minecraftUuid) { - return await accountsCollection?.findOne({ minecraftUuid }); -} -export async function fetchAccountFromDiscord(discordId) { - return await accountsCollection?.findOne({ discordId }); -} -export async function updateAccount(discordId, schema) { - await accountsCollection?.updateOne({ - discordId - }, { $set: schema }, { upsert: true }); -} -// make sure it's not in a test -console.log('global.isTest', globalThis.isTest); -if (!globalThis.isTest) { - connect().then(() => { - // when it connects, cache the leaderboards and remove bad members - removeBadMemberLeaderboardAttributes(); - // cache leaderboards on startup so its faster later on - fetchAllLeaderboards(true); - // cache leaderboard players again every 4 hours - setInterval(fetchAllLeaderboards, 4 * 60 * 60 * 1000); - }); -} diff --git a/build/discord.js b/build/discord.js deleted file mode 100644 index bf906a9..0000000 --- a/build/discord.js +++ /dev/null @@ -1,37 +0,0 @@ -import fetch from 'node-fetch'; -import { Agent } from 'https'; -const DISCORD_CLIENT_ID = '885347559382605916'; -const httpsAgent = new Agent({ - keepAlive: true -}); -export async function exchangeCode(redirectUri, code) { - const API_ENDPOINT = 'https://discord.com/api/v6'; - const CLIENT_SECRET = process.env.discord_client_secret; - if (!CLIENT_SECRET) { - console.error('discord_client_secret isn\'t in env, couldn\'t login with discord'); - return null; - } - const data = { - 'client_id': DISCORD_CLIENT_ID, - 'client_secret': CLIENT_SECRET, - 'grant_type': 'authorization_code', - 'code': code, - 'redirect_uri': redirectUri, - 'scope': 'identify' - }; - const fetchResponse = await fetch(API_ENDPOINT + '/oauth2/token', { - method: 'POST', - agent: () => httpsAgent, - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams(data).toString() - }); - return await fetchResponse.json(); -} -export async function getUser(accessToken) { - const API_ENDPOINT = 'https://discord.com/api/v6'; - const response = await fetch(API_ENDPOINT + '/users/@me', { - headers: { 'Authorization': 'Bearer ' + accessToken }, - agent: () => httpsAgent, - }); - return await response.json(); -} diff --git a/build/hypixel.js b/build/hypixel.js deleted file mode 100644 index 6a1a916..0000000 --- a/build/hypixel.js +++ /dev/null @@ -1,190 +0,0 @@ -/** - * Fetch the clean Hypixel API - */ -import { cleanSkyblockProfileResponse } from './cleaners/skyblock/profile.js'; -import { fetchAccount, queueUpdateDatabaseMember, queueUpdateDatabaseProfile } from './database.js'; -import { chooseApiKey, sendApiRequest } from './hypixelApi.js'; -import { cleanSkyblockProfilesResponse } from './cleaners/skyblock/profiles.js'; -import { cleanPlayerResponse } from './cleaners/player.js'; -import * as cached from './hypixelCached.js'; -import { debug } from './index.js'; -// 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; -/** Sends an API request to Hypixel and cleans it up. */ -export async function sendCleanApiRequest({ path, args }, included, options) { - 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 new Promise(resolve => setTimeout(resolve, 1000)); - return await sendCleanApiRequest({ path, args }, included, options); - } - // clean the response - return await cleanResponse({ path, data: rawResponse }, options ?? {}); -} -async function cleanResponse({ path, data }, options) { - // 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); - } -} -/** - * 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 }, included = ['player'], customization) { - if (!uuid) { - // If the uuid isn't provided, get it - if (!username && !user) - return null; - uuid = await cached.uuidFromUser((user ?? username)); - } - if (!uuid) { - // the user doesn't exist. - if (debug) - console.debug('error:', user, 'doesnt exist'); - return null; - } - const websiteAccountPromise = customization ? fetchAccount(uuid) : null; - const includePlayers = included.includes('player'); - const includeProfiles = included.includes('profiles'); - let profilesData; - let basicProfilesData; - let playerData = null; - if (includePlayers) { - playerData = await cached.fetchPlayer(uuid); - // if not including profiles, include lightweight profiles just in case - if (!includeProfiles) - basicProfilesData = playerData?.profiles; - if (playerData) - delete playerData.profiles; - } - if (includeProfiles) - profilesData = await cached.fetchSkyblockProfiles(uuid); - let activeProfile; - let lastOnline = 0; - if (includeProfiles) { - for (const profile of profilesData) { - const member = profile.members?.find(member => member.uuid === uuid); - if (member && member.last_save > lastOnline) { - lastOnline = member.last_save; - activeProfile = profile; - } - } - } - let websiteAccount = null; - if (websiteAccountPromise) - websiteAccount = await websiteAccountPromise; - return { - player: playerData, - profiles: profilesData ?? basicProfilesData, - activeProfile: includeProfiles ? activeProfile?.uuid : undefined, - online: includeProfiles ? lastOnline > (Date.now() - saveInterval) : undefined, - customization: websiteAccount?.customization - }; -} -/** - * 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 - * @param customization Whether stuff like the user's custom background will be returned - */ -export async function fetchMemberProfile(user, profile, customization) { - const playerUuid = await cached.uuidFromUser(user); - if (!playerUuid) - return null; - // we don't await the promise immediately so it can load while we do other stuff - const websiteAccountPromise = customization ? fetchAccount(playerUuid) : null; - const profileUuid = await cached.fetchProfileUuid(user, profile); - // if the profile or player doesn't have an id, just return - if (!profileUuid) - return null; - if (!playerUuid) - return null; - const player = await cached.fetchPlayer(playerUuid); - if (!player) - return null; // this should never happen, but if it does just return null - const cleanProfile = await cached.fetchProfile(playerUuid, profileUuid); - const member = cleanProfile.members.find(m => m.uuid === playerUuid); - if (!member) - return null; // this should never happen, but if it does just return null - // remove unnecessary member data - const simpleMembers = cleanProfile.members.map(m => { - return { - uuid: m.uuid, - username: m.username, - first_join: m.first_join, - last_save: m.last_save, - rank: m.rank - }; - }); - cleanProfile.members = simpleMembers; - let websiteAccount = null; - if (websiteAccountPromise) - websiteAccount = await websiteAccountPromise; - return { - member: { - // the profile name is in member rather than profile since they sometimes differ for each member - profileName: cleanProfile.name, - // add all the member data - ...member, - // add all other data relating to the hypixel player, such as username, rank, etc - ...player - }, - profile: cleanProfile, - customization: websiteAccount?.customization - }; -} -/** - * Fetches the Hypixel API to get a CleanFullProfile. This doesn't do any caching and you should use hypixelCached.fetchProfile instead - * @param playerUuid The UUID of the Minecraft player - * @param profileUuid The UUID of the Hypixel SkyBlock profile - */ -export async function fetchMemberProfileUncached(playerUuid, profileUuid) { - const profile = await sendCleanApiRequest({ - path: 'skyblock/profile', - args: { profile: profileUuid } - }, undefined, { mainMemberUuid: playerUuid }); - // queue updating the leaderboard positions for the member, eventually - for (const member of profile.members) - queueUpdateDatabaseMember(member, profile); - queueUpdateDatabaseProfile(profile); - return profile; -} -/** - * Fetches the Hypixel API to get a CleanProfile from its id. This doesn't do any caching and you should use hypixelCached.fetchBasicProfileFromUuid instead - * @param playerUuid The UUID of the Minecraft player - * @param profileUuid The UUID of the Hypixel SkyBlock profile - */ -export async function fetchBasicProfileFromUuidUncached(profileUuid) { - const profile = await sendCleanApiRequest({ - path: 'skyblock/profile', - args: { profile: profileUuid } - }, undefined, { basic: true }); - return profile; -} -export async function fetchMemberProfilesUncached(playerUuid) { - const profiles = await sendCleanApiRequest({ - path: 'skyblock/profiles', - args: { - uuid: playerUuid - } - }, undefined, { - // only the inventories for the main player are generated, this is for optimization purposes - mainMemberUuid: playerUuid - }); - for (const profile of profiles) { - for (const member of profile.members) { - queueUpdateDatabaseMember(member, profile); - } - queueUpdateDatabaseProfile(profile); - } - return profiles; -} diff --git a/build/hypixelApi.js b/build/hypixelApi.js deleted file mode 100644 index ae647b7..0000000 --- a/build/hypixelApi.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Fetch the raw Hypixel API - */ -import fetch from 'node-fetch'; -import { jsonToQuery, shuffle } from './util.js'; -import { Agent } from 'https'; -if (!process.env.hypixel_keys) - // if there's no hypixel keys in env, run dotenv - (await import('dotenv')).config(); -// We need to create an agent to prevent memory leaks and to only do dns lookups once -const httpsAgent = new Agent({ - keepAlive: true -}); -/** This array should only ever contain one item because using multiple hypixel api keys isn't allowed :) */ -const apiKeys = process.env?.hypixel_keys?.split(' ') ?? []; -const apiKeyUsage = {}; -const baseHypixelAPI = 'https://api.hypixel.net'; -/** Choose the best current API key */ -export function chooseApiKey() { - // find the api key with the lowest amount of uses - let bestKeyUsage = null; - let bestKey = null; - // we limit to 5 api keys because otherwise they get automatically banned - for (let key of shuffle(apiKeys.slice(0, 5))) { - const keyUsage = apiKeyUsage[key]; - // if the key has never been used before, use it - if (!keyUsage) - return key; - // if the key has reset since the last use, set the remaining count to the default - if (Date.now() > keyUsage.reset) - keyUsage.remaining = keyUsage.limit; - // if this key has more uses remaining than the current known best one, save it - if (bestKeyUsage === null || keyUsage.remaining > bestKeyUsage.remaining) { - bestKeyUsage = keyUsage; - bestKey = key; - } - } - return bestKey; -} -export function getKeyUsage() { - let keyLimit = 0; - let keyUsage = 0; - for (let key of Object.values(apiKeyUsage)) { - keyLimit += key.limit; - keyUsage += key.limit - key.remaining; - } - return { - limit: keyLimit, - usage: keyUsage - }; -} -/** Send an HTTP request to the Hypixel API */ -export let sendApiRequest = async function sendApiRequest({ path, key, args }) { - // 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; - let fetchJsonParsed; - try { - fetchResponse = await fetch(fetchUrl, { agent: () => httpsAgent }); - fetchJsonParsed = await fetchResponse.json(); - } - catch { - // if there's an error, wait a second and try again - await new Promise((resolve) => setTimeout(resolve, 1000)); - return await sendApiRequest({ path, key, args }); - } - // bruh - if (fetchJsonParsed.cause === 'This endpoint is currently disabled') { - await new Promise((resolve) => setTimeout(resolve, 30000)); - return await sendApiRequest({ path, key, args }); - } - // if the cause is "Invalid API key", remove the key from the list of keys and try again - if (fetchJsonParsed.cause === 'Invalid API key') { - if (apiKeys.includes(key)) { - apiKeys.splice(apiKeys.indexOf(key), 1); - console.log(`${key} is invalid, removing it from the list of keys`); - } - return await sendApiRequest({ path, key: chooseApiKey(), args }); - } - if (fetchResponse.headers.get('ratelimit-limit')) - // remember how many uses it has - apiKeyUsage[key] = { - remaining: parseInt(fetchResponse.headers.get('ratelimit-remaining') ?? '0'), - limit: parseInt(fetchResponse.headers.get('ratelimit-limit') ?? '0'), - reset: Date.now() + parseInt(fetchResponse.headers.get('ratelimit-reset') ?? '0') * 1000 - }; - if (fetchJsonParsed.throttle) { - if (apiKeyUsage[key]) - apiKeyUsage[key].remaining = 0; - // if it's throttled, wait 10 seconds and try again - await new Promise((resolve) => setTimeout(resolve, 10000)); - return await sendApiRequest({ path, key, args }); - } - return fetchJsonParsed; -}; -// this is necessary for mocking in the tests because es6 -export function mockSendApiRequest($value) { sendApiRequest = $value; } diff --git a/build/hypixelCached.js b/build/hypixelCached.js deleted file mode 100644 index 69bcc62..0000000 --- a/build/hypixelCached.js +++ /dev/null @@ -1,339 +0,0 @@ -/** - * Fetch the clean and cached Hypixel API - */ -import { isUuid, undashUuid } from './util.js'; -import * as hypixel from './hypixel.js'; -import * as mojang from './mojang.js'; -import NodeCache from 'node-cache'; -import { debug } from './index.js'; -import LRUCache from 'lru-cache'; -// cache usernames for 30 minutes -/** uuid: username */ -export const usernameCache = new NodeCache({ - // stdTTL: 60 * 60 * 4, - stdTTL: 60 * 30, - checkperiod: 60, - useClones: false, -}); -usernameCache.setMaxListeners(50); -export const basicProfilesCache = new NodeCache({ - stdTTL: 60 * 10, - checkperiod: 60, - useClones: true, -}); -export const playerCache = new NodeCache({ - stdTTL: 60, - checkperiod: 10, - useClones: true, -}); -// cache "basic players" (players without profiles) for 20 minutes -export const basicPlayerCache = new LRUCache({ - max: 10000, - maxAge: 60 * 20 * 1000, -}); -export const profileCache = new NodeCache({ - stdTTL: 30, - checkperiod: 10, - useClones: true, -}); -export const profilesCache = new NodeCache({ - stdTTL: 60 * 3, - checkperiod: 10, - useClones: false, -}); -export const profileNameCache = new NodeCache({ - stdTTL: 60 * 60, - checkperiod: 60, - useClones: false, -}); -function waitForCacheSet(cache, key, value) { - return new Promise((resolve, reject) => { - const listener = (setKey, setValue) => { - if (((setKey === key) || (value && setValue === value)) && typeof setValue === 'string') { - cache.removeListener('set', listener); - return resolve({ key: setKey, value: setValue }); - } - }; - cache.on('set', listener); - }); -} -/** - * Fetch the uuid from a user - * @param user A user can be either a uuid or a username - */ -export async function uuidFromUser(user) { - // if the user is 32 characters long, it has to be a uuid - if (isUuid(user)) - return undashUuid(user); - if (usernameCache.has(undashUuid(user))) { - // check if the uuid is a key - const username = usernameCache.get(undashUuid(user)); - // sometimes the username will be null, return that - if (username === null) - return undefined; - // if it has .then, then that means its a waitForCacheSet promise. This is done to prevent requests made while it is already requesting - if (username.then) { - const { key: uuid, value: _username } = await username; - usernameCache.set(uuid, _username); - return uuid; - } - else - return undashUuid(user); - } - // check if the username is a value - const uuidToUsername = usernameCache.mget(usernameCache.keys()); - for (const [uuid, username] of Object.entries(uuidToUsername)) { - if (username && username.toLowerCase && user.toLowerCase() === username.toLowerCase()) - return uuid; - } - if (debug) - console.debug('Cache miss: uuidFromUser', user); - const undashedUser = undashUuid(user); - // set it as waitForCacheSet (a promise) in case uuidFromUser gets called while its fetching mojang - usernameCache.set(undashedUser, waitForCacheSet(usernameCache, user, user)); - // not cached, actually fetch mojang api now - let { uuid, username } = await mojang.profileFromUser(user); - if (!uuid) { - usernameCache.set(user, null); - return; - } - // remove dashes from the uuid so its more normal - uuid = undashUuid(uuid); - usernameCache.del(undashedUser); - usernameCache.set(uuid, username); - return uuid; -} -/** - * Fetch the username from a user - * @param user A user can be either a uuid or a username - */ -export async function usernameFromUser(user) { - if (usernameCache.has(undashUuid(user))) { - if (debug) - console.debug('Cache hit! usernameFromUser', user); - return usernameCache.get(undashUuid(user)) ?? null; - } - if (debug) - console.debug('Cache miss: usernameFromUser', user); - let { uuid, username } = await mojang.profileFromUser(user); - if (!uuid) - return null; - uuid = undashUuid(uuid); - usernameCache.set(uuid, username); - return username; -} -let fetchingPlayers = new Set(); -export async function fetchPlayer(user) { - const playerUuid = await uuidFromUser(user); - if (!playerUuid) - return null; - if (playerCache.has(playerUuid)) - return playerCache.get(playerUuid); - // if it's already in the process of fetching, check every 100ms until it's not fetching the player anymore and fetch it again, since it'll be cached now - if (fetchingPlayers.has(playerUuid)) { - while (fetchingPlayers.has(playerUuid)) { - await new Promise(resolve => setTimeout(resolve, 100)); - } - return await fetchPlayer(user); - } - fetchingPlayers.add(playerUuid); - const cleanPlayer = await hypixel.sendCleanApiRequest({ - path: 'player', - args: { uuid: playerUuid } - }); - fetchingPlayers.delete(playerUuid); - if (!cleanPlayer) - return null; - // clone in case it gets modified somehow later - playerCache.set(playerUuid, cleanPlayer); - usernameCache.set(playerUuid, cleanPlayer.username); - const cleanBasicPlayer = Object.assign({}, cleanPlayer); - delete cleanBasicPlayer.profiles; - basicPlayerCache.set(playerUuid, cleanBasicPlayer); - return cleanPlayer; -} -/** Fetch a player without their profiles. This is heavily cached. */ -export async function fetchBasicPlayer(user) { - const playerUuid = await uuidFromUser(user); - if (!playerUuid) - return null; - if (basicPlayerCache.has(playerUuid)) - return basicPlayerCache.get(playerUuid); - const player = await fetchPlayer(playerUuid); - if (!player) { - console.debug('no player? this should never happen, perhaps the uuid is invalid or the player hasn\'t played hypixel', playerUuid); - return null; - } - delete player.profiles; - return player; -} -export async function fetchSkyblockProfiles(playerUuid) { - if (profilesCache.has(playerUuid)) { - if (debug) - console.debug('Cache hit! fetchSkyblockProfiles', playerUuid); - return profilesCache.get(playerUuid); - } - if (debug) - console.debug('Cache miss: fetchSkyblockProfiles', playerUuid); - const profiles = await hypixel.fetchMemberProfilesUncached(playerUuid); - const basicProfiles = []; - // create the basicProfiles array - for (const profile of profiles) { - const basicProfile = { - name: profile.name, - uuid: profile.uuid, - members: profile.members?.map(m => { - return { - uuid: m.uuid, - username: m.username, - first_join: m.first_join, - last_save: m.last_save, - rank: m.rank - }; - }) - }; - basicProfiles.push(basicProfile); - } - // cache the profiles - profilesCache.set(playerUuid, basicProfiles); - return basicProfiles; -} -/** Fetch an array of `BasicProfile`s */ -async function fetchBasicProfiles(user) { - const playerUuid = await uuidFromUser(user); - if (!playerUuid) - return null; // invalid player, just return - if (basicProfilesCache.has(playerUuid)) { - if (debug) - console.debug('Cache hit! fetchBasicProfiles', playerUuid); - return basicProfilesCache.get(playerUuid); - } - if (debug) - console.debug('Cache miss: fetchBasicProfiles', user); - const player = await fetchPlayer(playerUuid); - if (!player) { - console.log('bruh playerUuid', user, playerUuid); - return []; - } - const profiles = player.profiles; - basicProfilesCache.set(playerUuid, profiles); - if (!profiles) - return null; - // cache the profile names and uuids to profileNameCache because we can - for (const profile of profiles) - profileNameCache.set(`${playerUuid}.${profile.uuid}`, profile.name); - return profiles; -} -/** - * Fetch a profile UUID from its name and user - * @param user A username or uuid - * @param profile A profile name or profile uuid - */ -export async function fetchProfileUuid(user, profile) { - // if a profile wasn't provided, return - if (!profile) { - if (debug) - console.debug('no profile provided?', user, profile); - return null; - } - if (debug) - console.debug('Cache miss: fetchProfileUuid', user, profile); - const profiles = await fetchBasicProfiles(user); - if (!profiles) - return null; // user probably doesnt exist - const profileUuid = undashUuid(profile); - for (const p of profiles) { - if (p.name?.toLowerCase() === profileUuid.toLowerCase()) - return undashUuid(p.uuid); - else if (undashUuid(p.uuid) === undashUuid(profileUuid)) - return undashUuid(p.uuid); - } - return null; -} -/** - * Fetch an entire profile from the user and profile data - * @param user A username or uuid - * @param profile A profile name or profile uuid - */ -export async function fetchProfile(user, profile) { - const playerUuid = await uuidFromUser(user); - if (!playerUuid) - return null; - const profileUuid = await fetchProfileUuid(playerUuid, profile); - if (!profileUuid) - return null; - if (profileCache.has(profileUuid)) { - // we have the profile cached, return it :) - if (debug) - console.debug('Cache hit! fetchProfile', profileUuid); - return profileCache.get(profileUuid); - } - if (debug) - console.debug('Cache miss: fetchProfile', user, profile); - const profileName = await fetchProfileName(user, profile); - if (!profileName) - return null; // uhh this should never happen but if it does just return null - const cleanProfile = await hypixel.fetchMemberProfileUncached(playerUuid, profileUuid); - // we know the name from fetchProfileName, so set it here - cleanProfile.name = profileName; - profileCache.set(profileUuid, cleanProfile); - return cleanProfile; -} -/** - * Fetch a CleanProfile from the uuid - * @param profileUuid A profile name or profile uuid -*/ -export async function fetchBasicProfileFromUuid(profileUuid) { - if (profileCache.has(profileUuid)) { - // we have the profile cached, return it :) - if (debug) - console.debug('Cache hit! fetchBasicProfileFromUuid', profileUuid); - const profile = profileCache.get(profileUuid); - if (!profile) - return undefined; - return { - uuid: profile.uuid, - members: profile.members.map(m => ({ - uuid: m.uuid, - username: m.username, - last_save: m.last_save, - first_join: m.first_join, - rank: m.rank, - })), - name: profile.name - }; - } - // TODO: cache this - return await hypixel.fetchBasicProfileFromUuidUncached(profileUuid); -} -/** - * Fetch the name of a profile from the user and profile uuid - * @param user A player uuid or username - * @param profile A profile uuid or name - */ -export async function fetchProfileName(user, profile) { - // we're fetching the profile and player uuid again in case we were given a name, but it's cached so it's not much of a problem - const profileUuid = await fetchProfileUuid(user, profile); - if (!profileUuid) - return null; - const playerUuid = await uuidFromUser(user); - if (!playerUuid) - return null; - if (profileNameCache.has(`${playerUuid}.${profileUuid}`)) { - // Return the profile name if it's cached - if (debug) - console.debug('Cache hit! fetchProfileName', profileUuid); - return profileNameCache.get(`${playerUuid}.${profileUuid}`) ?? null; - } - if (debug) - console.debug('Cache miss: fetchProfileName', user, profile); - const basicProfiles = await fetchBasicProfiles(playerUuid); - if (!basicProfiles) - return null; - let profileName = null; - for (const basicProfile of basicProfiles) - if (basicProfile.uuid === playerUuid) - profileName = basicProfile.name ?? null; - profileNameCache.set(`${playerUuid}.${profileUuid}`, profileName); - return profileName; -} diff --git a/build/index.js b/build/index.js deleted file mode 100644 index 050e22a..0000000 --- a/build/index.js +++ /dev/null @@ -1,169 +0,0 @@ -import { createSession, fetchAccountFromDiscord, fetchAllLeaderboardsCategorized, fetchLeaderboard, fetchMemberLeaderboardSpots, fetchSession, finishedCachingRawLeaderboards, leaderboardUpdateMemberQueue, leaderboardUpdateProfileQueue, updateAccount } from './database.js'; -import { fetchMemberProfile, fetchUser } from './hypixel.js'; -import rateLimit from 'express-rate-limit'; -import * as constants from './constants.js'; -import * as discord from './discord.js'; -import express from 'express'; -import { basicPlayerCache, basicProfilesCache, playerCache, profileCache, profileNameCache, profilesCache, usernameCache } from './hypixelCached.js'; -const app = express(); -export const debug = false; -const mainSiteUrl = 'https://skyblock.matdoes.dev'; -// 200 requests over 5 minutes -const limiter = rateLimit({ - windowMs: 60 * 1000 * 5, - max: 200, - skip: (req) => { - return req.headers.key === process.env.key; - }, - keyGenerator: (req) => { - return (req.headers['cf-connecting-ip'] ?? req.ip).toString(); - } -}); -app.use(limiter); -app.use(express.json()); -app.use((req, res, next) => { - res.setHeader('Access-Control-Allow-Origin', '*'); - next(); -}); -const startTime = Date.now(); -app.get('/', async (req, res) => { - const currentTime = Date.now(); - res.json({ - ok: true, - uptimeHours: (currentTime - startTime) / 1000 / 60 / 60, - finishedCachingRawLeaderboards, - leaderboardUpdateMemberQueueSize: leaderboardUpdateMemberQueue.size, - leaderboardUpdateProfileQueueSize: leaderboardUpdateProfileQueue.size, - usernameCacheSize: usernameCache.keys().length, - basicProfilesCacheSize: basicProfilesCache.keys().length, - playerCacheSize: playerCache.keys().length, - basicPlayerCacheSize: basicPlayerCache.keys().length, - profileCacheSize: profileCache.keys().length, - profilesCacheSize: profilesCache.keys().length, - profileNameCacheSize: profileNameCache.keys().length, - // key: getKeyUsage() - }); -}); -app.get('/player/:user', async (req, res) => { - try { - const user = await fetchUser({ user: req.params.user }, [req.query.basic === 'true' ? undefined : 'profiles', 'player'], req.query.customization === 'true'); - if (user) - res.json(user); - else - res.status(404).json({ error: true }); - } - catch (err) { - console.error(err); - res.json({ error: true }); - } -}); -app.get('/discord/:id', async (req, res) => { - try { - res.json(await fetchAccountFromDiscord(req.params.id)); - } - catch (err) { - console.error(err); - res.json({ ok: false }); - } -}); -app.get('/player/:user/:profile', async (req, res) => { - try { - const profile = await fetchMemberProfile(req.params.user, req.params.profile, req.query.customization === 'true'); - if (profile) - res.json(profile); - else - res.status(404).json({ error: true }); - } - catch (err) { - console.error(err); - res.json({ error: true }); - } -}); -app.get('/player/:user/:profile/leaderboards', async (req, res) => { - try { - res.json(await fetchMemberLeaderboardSpots(req.params.user, req.params.profile)); - } - catch (err) { - console.error(err); - res.json({ ok: false }); - } -}); -app.get('/leaderboard/:name', async (req, res) => { - try { - res.json(await fetchLeaderboard(req.params.name)); - } - catch (err) { - console.error(err); - res.json({ 'error': err.toString() }); - } -}); -app.get('/leaderboards', async (req, res) => { - try { - res.json(await fetchAllLeaderboardsCategorized()); - } - catch (err) { - console.error(err); - res.json({ ok: false }); - } -}); -app.get('/constants', async (req, res) => { - try { - res.json(await constants.fetchConstantValues()); - } - catch (err) { - console.error(err); - res.json({ ok: false }); - } -}); -app.post('/accounts/createsession', async (req, res) => { - try { - const { code } = req.body; - const codeExchange = await discord.exchangeCode(`${mainSiteUrl}/loggedin`, code); - if (!codeExchange) { - res.json({ ok: false, error: 'discord_client_secret isn\'t in env' }); - return; - } - const { access_token: accessToken, refresh_token: refreshToken } = codeExchange; - if (!accessToken) - // access token is invalid :( - return res.json({ ok: false }); - const userData = await discord.getUser(accessToken); - const sessionId = await createSession(refreshToken, userData); - res.json({ ok: true, session_id: sessionId }); - } - catch (err) { - res.json({ ok: false }); - } -}); -app.post('/accounts/session', async (req, res) => { - try { - const { uuid } = req.body; - const session = await fetchSession(uuid); - if (!session) - return res.json({ ok: false }); - const account = await fetchAccountFromDiscord(session.discord_user.id); - res.json({ session, account }); - } - catch (err) { - console.error(err); - res.json({ ok: false }); - } -}); -app.post('/accounts/update', async (req, res) => { - // it checks against the key, so it's kind of secure - if (req.headers.key !== process.env.key) - return console.log('bad key!'); - try { - await updateAccount(req.body.discordId, req.body); - res.json({ ok: true }); - } - catch (err) { - console.error(err); - res.json({ ok: false }); - } -}); -process.on('uncaughtException', err => console.error(err)); -process.on('unhandledRejection', (err, promise) => console.error(promise, err)); -// only run the server if it's not doing tests -if (!globalThis.isTest) - app.listen(8080, () => console.log('App started :)')); diff --git a/build/mojang.js b/build/mojang.js deleted file mode 100644 index 7682839..0000000 --- a/build/mojang.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Fetch the Mojang username API through api.ashcon.app - */ -import { isUuid, undashUuid } from './util.js'; -import fetch from 'node-fetch'; -import { Agent } from 'https'; -// We need to create an agent to prevent memory leaks -const httpsAgent = new Agent({ - keepAlive: true -}); -/** - * Get mojang api data from the session server - */ -export let profileFromUuid = async function profileFromUuid(uuid) { - let fetchResponse; - try { - fetchResponse = await fetch( - // using mojang directly is faster than ashcon lol, also mojang removed the ratelimits from here - `https://sessionserver.mojang.com/session/minecraft/profile/${undashUuid(uuid)}`, { agent: () => httpsAgent }); - } - catch { - // if there's an error, wait a second and try again - await new Promise((resolve) => setTimeout(resolve, 1000)); - return await profileFromUuid(uuid); - } - let dataString; - try { - dataString = await fetchResponse.text(); - } - catch (err) { - return { uuid: null, username: null }; - } - let data; - try { - data = JSON.parse(dataString); - } - catch { - // if it errors, just return null - return { uuid: null, username: null }; - } - return { - uuid: data.id, - username: data.name - }; -}; -export let profileFromUsername = async function profileFromUsername(username) { - // since we don't care about anything other than the uuid, we can use /uuid/ instead of /user/ - let fetchResponse; - try { - fetchResponse = await fetch(`https://api.mojang.com/users/profiles/minecraft/${username}`, { agent: () => httpsAgent }); - } - catch { - // if there's an error, wait a second and try again - await new Promise((resolve) => setTimeout(resolve, 1000)); - return await profileFromUsername(username); - } - let data = null; - const rawData = await fetchResponse.text(); - try { - data = JSON.parse(rawData); - } - catch { } - if (!data?.id) { - // return { uuid: null, username: null } - return await profileFromUsernameAlternative(username); - } - return { - uuid: data.id, - username: data.name - }; -}; -export async function profileFromUsernameAlternative(username) { - let fetchResponse; - try { - fetchResponse = await fetch(`https://api.ashcon.app/mojang/v2/user/${username}`, { agent: () => httpsAgent }); - } - catch { - // if there's an error, wait a second and try again - await new Promise((resolve) => setTimeout(resolve, 1000)); - return await profileFromUsernameAlternative(username); - } - let data; - try { - data = await fetchResponse.json(); - } - catch { - return { uuid: null, username: null }; - } - if (!data.uuid) - return { uuid: null, username: null }; - return { - uuid: undashUuid(data.uuid), - username: data.username - }; -} -export let profileFromUser = async function profileFromUser(user) { - if (isUuid(user)) { - return await profileFromUuid(user); - } - else - return await profileFromUsername(user); -}; -// this is necessary for mocking in the tests because es6 -export function mockProfileFromUuid($value) { profileFromUuid = $value; } -export function mockProfileFromUsername($value) { profileFromUsername = $value; } -export function mockProfileFromUser($value) { profileFromUser = $value; } diff --git a/build/util.js b/build/util.js deleted file mode 100644 index e78e390..0000000 --- a/build/util.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Random utility functions that are not related to Hypixel - */ -export function undashUuid(uuid) { - return uuid.replace(/-/g, '').toLowerCase(); -} -export function jsonToQuery(data) { - return Object.entries(data || {}).map(e => e.join('=')).join('&'); -} -export function shuffle(a) { - for (let i = a.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [a[i], a[j]] = [a[j], a[i]]; - } - return a; -} -export const minecraftColorCodes = { - '0': '#000000', - '1': '#0000be', - '2': '#00be00', - '3': '#00bebe', - '4': '#be0000', - '5': '#be00be', - '6': '#ffaa00', - '7': '#bebebe', - '8': '#3f3f3f', - '9': '#3f3ffe', - 'a': '#3ffe3f', - 'b': '#3ffefe', - 'c': '#fe3f3f', - 'd': '#fe3ffe', - 'e': '#fefe3f', - 'f': '#ffffff', - 'black': '#000000', - 'dark_blue': '#0000be', - 'dark_green': '#00be00', - 'dark_aqua': '#00bebe', - 'dark_red': '#be0000', - 'dark_purple': '#be00be', - 'gold': '#ffaa00', - 'gray': '#bebebe', - 'dark_gray': '#3f3f3f', - 'blue': '#3f3ffe', - 'green': '#3ffe3f', - 'aqua': '#3ffefe', - 'red': '#fe3f3f', - 'light_purple': '#fe3ffe', - 'yellow': '#fefe3f', - 'white': '#ffffff', -}; -/** - * Converts a color name to the code - * For example: blue -> 9 - * @param colorName The name of the color (blue, red, aqua, etc) - */ -export function colorCodeFromName(colorName) { - const hexColor = minecraftColorCodes[colorName.toLowerCase()]; - for (const key in minecraftColorCodes) { - const value = minecraftColorCodes[key]; - if (key.length === 1 && value === hexColor) - return key; - } -} -export async function sleep(ms) { - await new Promise(resolve => setTimeout(resolve, ms)); -} -/** Returns whether a string is a UUID4 (Minecraft uuid) */ -export function isUuid(string) { - return undashUuid(string).length === 32; -} |