aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/cleaners/player.js17
-rw-r--r--build/cleaners/rank.js81
-rw-r--r--build/cleaners/skyblock/bank.js8
-rw-r--r--build/cleaners/skyblock/collections.js116
-rw-r--r--build/cleaners/skyblock/fairysouls.js7
-rw-r--r--build/cleaners/skyblock/inventory.js83
-rw-r--r--build/cleaners/skyblock/itemId.js57
-rw-r--r--build/cleaners/skyblock/member.js54
-rw-r--r--build/cleaners/skyblock/minions.js85
-rw-r--r--build/cleaners/skyblock/objectives.js12
-rw-r--r--build/cleaners/skyblock/profile.js64
-rw-r--r--build/cleaners/skyblock/profiles.js21
-rw-r--r--build/cleaners/skyblock/skills.js146
-rw-r--r--build/cleaners/skyblock/slayers.js60
-rw-r--r--build/cleaners/skyblock/stats.js91
-rw-r--r--build/cleaners/skyblock/zones.js23
-rw-r--r--build/cleaners/socialmedia.js6
-rw-r--r--build/constants.js226
-rw-r--r--build/database.js600
-rw-r--r--build/discord.js37
-rw-r--r--build/hypixel.js190
-rw-r--r--build/hypixelApi.js101
-rw-r--r--build/hypixelCached.js339
-rw-r--r--build/index.js169
-rw-r--r--build/mojang.js106
-rw-r--r--build/util.js70
-rw-r--r--src/index.ts8
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) => {