diff options
| author | mat <github@matdoes.dev> | 2021-12-30 16:18:57 -0600 | 
|---|---|---|
| committer | mat <github@matdoes.dev> | 2021-12-30 16:18:57 -0600 | 
| commit | 342d31e99d1fbd7bcc941e34bb2a175382f3e26e (patch) | |
| tree | c735fa67652d49386d3b55b42567ea3ebd57c5c4 | |
| parent | f7d1263ea669968a4b69d80cede9b77f240de4b6 (diff) | |
| download | skyblock-api-342d31e99d1fbd7bcc941e34bb2a175382f3e26e.tar.gz skyblock-api-342d31e99d1fbd7bcc941e34bb2a175382f3e26e.tar.bz2 skyblock-api-342d31e99d1fbd7bcc941e34bb2a175382f3e26e.zip | |
remove build folder
27 files changed, 5 insertions, 2772 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; -} diff --git a/src/index.ts b/src/index.ts index ffa683e..3924c60 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,7 @@ app.use((req, res, next) => {  const startTime = Date.now()  app.get('/', async (req, res) => {  	const currentTime = Date.now() -	res.json({ +	let data: any = {  		ok: true,  		uptimeHours: (currentTime - startTime) / 1000 / 60 / 60,  		finishedCachingRawLeaderboards, @@ -50,8 +50,10 @@ app.get('/', async (req, res) => {  		profileCacheSize: profileCache.keys().length,  		profilesCacheSize: profilesCache.keys().length,  		profileNameCacheSize: profileNameCache.keys().length, -		// key: getKeyUsage() -	}) +	} +	if (req.headers.key === process.env.key) +		data.key = getKeyUsage() +	res.json()  })  app.get('/player/:user', async (req, res) => { | 
