diff options
author | mat <27899617+mat-1@users.noreply.github.com> | 2021-04-27 20:02:26 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-27 20:02:26 -0500 |
commit | 39210646284cabc89256466646f8ea7834a78d31 (patch) | |
tree | e602e871a72681038e652907fefecf94ef82702f | |
parent | 2b309975eec24b09dab95076c433e9392dc2e3ed (diff) | |
download | skyblock-api-39210646284cabc89256466646f8ea7834a78d31.tar.gz skyblock-api-39210646284cabc89256466646f8ea7834a78d31.tar.bz2 skyblock-api-39210646284cabc89256466646f8ea7834a78d31.zip |
Add minion_count leaderboard (#15)
* add minion count leaderboard
* fix error that happens sometimes
* Update index.js
-rw-r--r-- | build/cleaners/skyblock/inventory.js | 1 | ||||
-rw-r--r-- | build/cleaners/skyblock/member.js | 2 | ||||
-rw-r--r-- | build/cleaners/skyblock/profile.js | 12 | ||||
-rw-r--r-- | build/database.js | 184 | ||||
-rw-r--r-- | build/hypixel.js | 17 | ||||
-rw-r--r-- | build/hypixelCached.js | 30 | ||||
-rw-r--r-- | build/index.js | 8 | ||||
-rw-r--r-- | src/cleaners/skyblock/inventory.ts | 1 | ||||
-rw-r--r-- | src/cleaners/skyblock/member.ts | 2 | ||||
-rw-r--r-- | src/cleaners/skyblock/profile.ts | 15 | ||||
-rw-r--r-- | src/database.ts | 248 | ||||
-rw-r--r-- | src/hypixel.ts | 26 | ||||
-rw-r--r-- | src/hypixelCached.ts | 28 | ||||
-rw-r--r-- | src/index.ts | 13 |
14 files changed, 532 insertions, 55 deletions
diff --git a/build/cleaners/skyblock/inventory.js b/build/cleaners/skyblock/inventory.js index 0131e1b..400408c 100644 --- a/build/cleaners/skyblock/inventory.js +++ b/build/cleaners/skyblock/inventory.js @@ -20,6 +20,7 @@ var __importStar = (this && this.__importStar) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); exports.cleanInventories = exports.INVENTORIES = exports.cleanInventory = void 0; +// maybe todo?: create a fast replacement for prismarine-nbt const nbt = __importStar(require("prismarine-nbt")); function base64decode(base64) { return Buffer.from(base64, 'base64'); diff --git a/build/cleaners/skyblock/member.js b/build/cleaners/skyblock/member.js index 9869646..ce322bc 100644 --- a/build/cleaners/skyblock/member.js +++ b/build/cleaners/skyblock/member.js @@ -45,7 +45,7 @@ exports.cleanSkyBlockProfileMemberResponseBasic = cleanSkyBlockProfileMemberResp async function cleanSkyBlockProfileMemberResponse(member, included = null) { var _a; // profiles.members[] - const inventoriesIncluded = included == null || included.includes('inventories'); + const inventoriesIncluded = included === null || included.includes('inventories'); const player = await cached.fetchPlayer(member.uuid); if (!player) return; diff --git a/build/cleaners/skyblock/profile.js b/build/cleaners/skyblock/profile.js index ef90ce8..ba2c030 100644 --- a/build/cleaners/skyblock/profile.js +++ b/build/cleaners/skyblock/profile.js @@ -31,9 +31,19 @@ async function cleanSkyblockProfileResponse(data, options) { for (const memberUUID in data.members) { const memberRaw = data.members[memberUUID]; memberRaw.uuid = memberUUID; - promises.push(member_1.cleanSkyBlockProfileMemberResponse(memberRaw, ['stats', (options === null || options === void 0 ? void 0 : options.mainMemberUuid) === memberUUID ? 'inventories' : undefined])); + promises.push(member_1.cleanSkyBlockProfileMemberResponse(memberRaw, [ + !(options === null || options === void 0 ? void 0 : options.basic) ? 'stats' : undefined, + (options === null || options === void 0 ? void 0 : options.mainMemberUuid) === memberUUID ? 'inventories' : undefined + ])); } const cleanedMembers = (await Promise.all(promises)).filter(m => m !== null && m !== undefined); + if (options === null || options === void 0 ? void 0 : options.basic) { + return { + uuid: data.profile_id, + name: data.cute_name, + members: cleanedMembers, + }; + } const memberMinions = []; for (const member of cleanedMembers) { memberMinions.push(member.minions); diff --git a/build/database.js b/build/database.js index be193e9..600b21d 100644 --- a/build/database.js +++ b/build/database.js @@ -25,7 +25,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.queueUpdateDatabaseMember = exports.updateDatabaseMember = exports.fetchMemberLeaderboardSpots = exports.fetchMemberLeaderboard = exports.fetchAllMemberLeaderboardAttributes = exports.fetchSlayerLeaderboards = exports.fetchAllLeaderboardsCategorized = void 0; +exports.queueUpdateDatabaseProfile = exports.queueUpdateDatabaseMember = exports.updateDatabaseProfile = exports.updateDatabaseMember = exports.fetchMemberLeaderboardSpots = exports.fetchLeaderboard = exports.fetchProfileLeaderboard = exports.fetchMemberLeaderboard = exports.fetchAllMemberLeaderboardAttributes = exports.fetchSlayerLeaderboards = exports.fetchAllLeaderboardsCategorized = void 0; const stats_1 = require("./cleaners/skyblock/stats"); const mongodb_1 = require("mongodb"); const cached = __importStar(require("./hypixelCached")); @@ -50,6 +50,7 @@ const reversedLeaderboards = [ let client; let database; let memberLeaderboardsCollection; +let profileLeaderboardsCollection; 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.'); @@ -58,6 +59,7 @@ async function connect() { client = await mongodb_1.MongoClient.connect(process.env.db_uri, { useNewUrlParser: true, useUnifiedTopology: true }); database = client.db(process.env.db_name); memberLeaderboardsCollection = database.collection('member-leaderboards'); + profileLeaderboardsCollection = database.collection('profile-leaderboards'); } function getMemberCollectionAttributes(member) { const collectionAttributes = {}; @@ -106,10 +108,17 @@ function getMemberLeaderboardAttributes(member) { 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 { + minion_count: profile.minion_count + }; +} async function fetchAllLeaderboardsCategorized() { const memberLeaderboardAttributes = await fetchAllMemberLeaderboardAttributes(); + const profileLeaderboardAttributes = await fetchAllProfileLeaderboardAttributes(); const categorizedLeaderboards = {}; - for (const leaderboard of memberLeaderboardAttributes) { + for (const leaderboard of [...memberLeaderboardAttributes, ...profileLeaderboardAttributes]) { const { category } = stats_1.categorizeStat(leaderboard); if (!categorizedLeaderboards[category]) categorizedLeaderboards[category] = []; @@ -140,7 +149,7 @@ async function fetchSlayerLeaderboards() { return leaderboardNames; } exports.fetchSlayerLeaderboards = fetchSlayerLeaderboards; -/** Fetch the names of all the leaderboards */ +/** Fetch the names of all the leaderboards that rank members */ 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 @@ -159,6 +168,12 @@ async function fetchAllMemberLeaderboardAttributes() { ]; } exports.fetchAllMemberLeaderboardAttributes = fetchAllMemberLeaderboardAttributes; +/** Fetch the names of all the leaderboards that rank profiles */ +async function fetchAllProfileLeaderboardAttributes() { + return [ + 'minion_count' + ]; +} function isLeaderboardReversed(name) { for (const leaderboardMatch of reversedLeaderboards) { let trailingEnd = leaderboardMatch[0] === '_'; @@ -171,6 +186,8 @@ function isLeaderboardReversed(name) { return false; } async function fetchMemberLeaderboardRaw(name) { + if (!client) + throw Error('Client isn\'t initialized yet'); 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 @@ -186,6 +203,23 @@ async function fetchMemberLeaderboardRaw(name) { cachedRawLeaderboards.set(name, leaderboardRaw); return leaderboardRaw; } +async function fetchProfileLeaderboardRaw(name) { + 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; + const leaderboardRaw = await profileLeaderboardsCollection + .find(query) + .sort(sortQuery) + .limit(leaderboardMax) + .toArray(); + console.log('leaderboardRaw', leaderboardRaw); + cachedRawLeaderboards.set(name, leaderboardRaw); + return leaderboardRaw; +} /** Fetch a leaderboard that ranks members, as opposed to profiles */ async function fetchMemberLeaderboard(name) { var _a; @@ -193,6 +227,7 @@ async function fetchMemberLeaderboard(name) { const fetchLeaderboardPlayer = async (item) => { return { player: await cached.fetchBasicPlayer(item.uuid), + profileUuid: item.profile, value: item.stats[name] }; }; @@ -208,6 +243,44 @@ async function fetchMemberLeaderboard(name) { }; } exports.fetchMemberLeaderboard = fetchMemberLeaderboard; +/** Fetch a leaderboard that ranks profiles, as opposed to members */ +async function fetchProfileLeaderboard(name) { + var _a; + const leaderboardRaw = await fetchProfileLeaderboardRaw(name); + const fetchLeaderboardProfile = async (item) => { + const players = []; + for (const playerUuid of item.players) + players.push(await cached.fetchBasicPlayer(playerUuid)); + return { + players: players, + profileUuid: item.uuid, + value: item.stats[name] + }; + }; + const promises = []; + for (const item of leaderboardRaw) { + promises.push(fetchLeaderboardProfile(item)); + } + const leaderboard = await Promise.all(promises); + return { + name: name, + unit: (_a = stats_1.getStatUnit(name)) !== null && _a !== void 0 ? _a : null, + list: leaderboard + }; +} +exports.fetchProfileLeaderboard = fetchProfileLeaderboard; +/** Fetch a leaderboard */ +async function fetchLeaderboard(name) { + const profileLeaderboards = await fetchAllProfileLeaderboardAttributes(); + console.log(name, profileLeaderboards, profileLeaderboards.includes(name)); + if (profileLeaderboards.includes(name)) { + return await fetchProfileLeaderboard(name); + } + else { + return await fetchMemberLeaderboard(name); + } +} +exports.fetchLeaderboard = fetchLeaderboard; /** Get the leaderboard positions a member is on. This may take a while depending on whether stuff is cached */ async function fetchMemberLeaderboardSpots(player, profile) { var _a; @@ -215,7 +288,7 @@ async function fetchMemberLeaderboardSpots(player, profile) { const fullMember = fullProfile.members.find(m => m.username.toLowerCase() === player.toLowerCase() || m.uuid === player); // update the leaderboard positions for the member await updateDatabaseMember(fullMember, fullProfile); - const applicableAttributes = await getApplicableAttributes(fullMember); + const applicableAttributes = await getApplicableMemberLeaderboardAttributes(fullMember); const memberLeaderboardSpots = []; for (const leaderboardName in applicableAttributes) { const leaderboard = await fetchMemberLeaderboardRaw(leaderboardName); @@ -230,8 +303,12 @@ async function fetchMemberLeaderboardSpots(player, profile) { return memberLeaderboardSpots; } exports.fetchMemberLeaderboardSpots = fetchMemberLeaderboardSpots; -async function getMemberLeaderboardRequirement(name) { - const leaderboard = await fetchMemberLeaderboardRaw(name); +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 if (leaderboard.length >= leaderboardMax) return leaderboard[leaderboardMax - 1].stats[name]; @@ -239,11 +316,31 @@ async function getMemberLeaderboardRequirement(name) { return null; } /** Get the attributes for the member, but only ones that would put them on the top 100 for leaderboards */ -async function getApplicableAttributes(member) { +async function getApplicableMemberLeaderboardAttributes(member) { const leaderboardAttributes = getMemberLeaderboardAttributes(member); const applicableAttributes = {}; for (const [leaderboard, attributeValue] of Object.entries(leaderboardAttributes)) { - const requirement = await getMemberLeaderboardRequirement(leaderboard); + const requirement = await getLeaderboardRequirement(leaderboard, 'member'); + const leaderboardReversed = isLeaderboardReversed(leaderboard); + if ((requirement === null) + || (leaderboardReversed ? attributeValue < requirement : attributeValue > requirement)) { + applicableAttributes[leaderboard] = attributeValue; + } + } + let leaderboardsCount = Object.keys(applicableAttributes).length; + const leaderboardsCountRequirement = await getLeaderboardRequirement('leaderboards_count', 'member'); + if ((leaderboardsCountRequirement === null) + || (leaderboardsCount > leaderboardsCountRequirement)) { + applicableAttributes['leaderboards_count'] = leaderboardsCount; + } + 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 = {}; + for (const [leaderboard, attributeValue] of Object.entries(leaderboardAttributes)) { + const requirement = await getLeaderboardRequirement(leaderboard, 'profile'); const leaderboardReversed = isLeaderboardReversed(leaderboard); if ((requirement === null) || (leaderboardReversed ? attributeValue < requirement : attributeValue > requirement)) { @@ -251,10 +348,9 @@ async function getApplicableAttributes(member) { } } let leaderboardsCount = Object.keys(applicableAttributes).length; - const leaderboardsCountRequirement = await getMemberLeaderboardRequirement('leaderboards_count'); + const leaderboardsCountRequirement = await getLeaderboardRequirement('leaderboards_count', 'member'); if ((leaderboardsCountRequirement === null) || (leaderboardsCount > leaderboardsCountRequirement)) { - // add 1 extra because this attribute also counts :) applicableAttributes['leaderboards_count'] = leaderboardsCount; } return applicableAttributes; @@ -279,9 +375,9 @@ async function updateDatabaseMember(member, profile) { await constants.addSlayers(member.slayers.bosses.map(s => s.raw_name)); if (_1.debug) console.log('done constants..'); - const leaderboardAttributes = await getApplicableAttributes(member); + const leaderboardAttributes = await getApplicableMemberLeaderboardAttributes(member); if (_1.debug) - console.log('done getApplicableAttributes..', leaderboardAttributes, member.username, profile.name); + console.log('done getApplicableMemberLeaderboardAttributes..', leaderboardAttributes, member.username, profile.name); await memberLeaderboardsCollection.updateOne({ uuid: member.uuid, profile: profile.uuid @@ -311,15 +407,73 @@ async function updateDatabaseMember(member, profile) { console.log('added member to leaderboards', member.username, leaderboardAttributes); } exports.updateDatabaseMember = updateDatabaseMember; -const leaderboardUpdateQueue = new queue_promise_1.default({ +/** + * 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 + */ +async function updateDatabaseProfile(profile) { + if (_1.debug) + console.log('updateDatabaseProfile', profile.name); + if (!client) + return; // the db client hasn't been initialized + // 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 (_1.debug) + console.log('adding profile to leaderboards', profile.name); + const leaderboardAttributes = await getApplicableProfileLeaderboardAttributes(profile); + if (_1.debug) + console.log('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([{ + last_updated: new Date(), + stats: leaderboardAttributes, + uuid: profile.uuid, + players: profile.members.map(p => p.uuid) + }]) + .sort((a, b) => leaderboardReverse ? a.stats[attributeName] - b.stats[attributeName] : b.stats[attributeName] - a.stats[attributeName]) + .slice(0, 100); + cachedRawLeaderboards.set(attributeName, newRawLeaderboard); + } + if (_1.debug) + console.log('added profile to leaderboards', profile.name, leaderboardAttributes); +} +exports.updateDatabaseProfile = updateDatabaseProfile; +const leaderboardUpdateMemberQueue = new queue_promise_1.default({ concurrent: 1, interval: 500 }); +const leaderboardUpdateProfileQueue = new queue_promise_1.default({ + concurrent: 1, + interval: 2000 +}); /** Queue an update for the member's leaderboard data on the server if applicable */ async function queueUpdateDatabaseMember(member, profile) { - leaderboardUpdateQueue.enqueue(async () => await updateDatabaseMember(member, profile)); + leaderboardUpdateMemberQueue.enqueue(async () => await updateDatabaseMember(member, profile)); } exports.queueUpdateDatabaseMember = queueUpdateDatabaseMember; +/** Queue an update for the profile's leaderboard data on the server if applicable */ +async function queueUpdateDatabaseProfile(profile) { + leaderboardUpdateProfileQueue.enqueue(async () => await updateDatabaseProfile(profile)); +} +exports.queueUpdateDatabaseProfile = queueUpdateDatabaseProfile; /** * Remove leaderboard attributes for members that wouldn't actually be on the leaderboard. This saves a lot of storage space */ @@ -332,7 +486,7 @@ async function removeBadMemberLeaderboardAttributes() { const unsetValue = {}; unsetValue[leaderboard] = ''; const filter = {}; - const requirement = await getMemberLeaderboardRequirement(leaderboard); + const requirement = await getLeaderboardRequirement(leaderboard, 'member'); const leaderboardReversed = isLeaderboardReversed(leaderboard); if (requirement !== null) { filter[`stats.${leaderboard}`] = { diff --git a/build/hypixel.js b/build/hypixel.js index 175ee79..9e9007e 100644 --- a/build/hypixel.js +++ b/build/hypixel.js @@ -22,7 +22,7 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.fetchMemberProfilesUncached = exports.fetchMemberProfileUncached = exports.fetchMemberProfile = exports.fetchUser = exports.sendCleanApiRequest = exports.maxMinion = exports.saveInterval = void 0; +exports.fetchMemberProfilesUncached = exports.fetchBasicProfileFromUuidUncached = exports.fetchMemberProfileUncached = exports.fetchMemberProfile = exports.fetchUser = exports.sendCleanApiRequest = exports.maxMinion = exports.saveInterval = void 0; const player_1 = require("./cleaners/player"); const hypixelApi_1 = require("./hypixelApi"); const cached = __importStar(require("./hypixelCached")); @@ -159,9 +159,23 @@ async function fetchMemberProfileUncached(playerUuid, profileUuid) { // queue updating the leaderboard positions for the member, eventually for (const member of profile.members) database_1.queueUpdateDatabaseMember(member, profile); + database_1.queueUpdateDatabaseProfile(profile); return profile; } exports.fetchMemberProfileUncached = fetchMemberProfileUncached; +/** + * 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 + */ +async function fetchBasicProfileFromUuidUncached(profileUuid) { + const profile = await sendCleanApiRequest({ + path: 'skyblock/profile', + args: { profile: profileUuid } + }, null, { basic: true }); + return profile; +} +exports.fetchBasicProfileFromUuidUncached = fetchBasicProfileFromUuidUncached; async function fetchMemberProfilesUncached(playerUuid) { const profiles = await sendCleanApiRequest({ path: 'skyblock/profiles', @@ -176,6 +190,7 @@ async function fetchMemberProfilesUncached(playerUuid) { for (const member of profile.members) { database_1.queueUpdateDatabaseMember(member, profile); } + database_1.queueUpdateDatabaseProfile(profile); } return profiles; } diff --git a/build/hypixelCached.js b/build/hypixelCached.js index 8693032..cad0e94 100644 --- a/build/hypixelCached.js +++ b/build/hypixelCached.js @@ -25,7 +25,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.fetchProfileName = exports.fetchProfile = exports.fetchProfileUuid = exports.fetchSkyblockProfiles = exports.fetchBasicPlayer = exports.fetchPlayer = exports.usernameFromUser = exports.uuidFromUser = exports.profileNameCache = exports.profilesCache = exports.profileCache = exports.basicPlayerCache = exports.playerCache = exports.basicProfilesCache = exports.usernameCache = void 0; +exports.fetchProfileName = exports.fetchBasicProfileFromUuid = exports.fetchProfile = exports.fetchProfileUuid = exports.fetchSkyblockProfiles = exports.fetchBasicPlayer = exports.fetchPlayer = exports.usernameFromUser = exports.uuidFromUser = exports.profileNameCache = exports.profilesCache = exports.profileCache = exports.basicPlayerCache = exports.playerCache = exports.basicProfilesCache = exports.usernameCache = void 0; const node_cache_1 = __importDefault(require("node-cache")); const mojang = __importStar(require("./mojang")); const hypixel = __importStar(require("./hypixel")); @@ -180,6 +180,8 @@ async function fetchBasicPlayer(user) { if (exports.basicPlayerCache.has(playerUuid)) return exports.basicPlayerCache.get(playerUuid); const player = await fetchPlayer(playerUuid); + if (!player) + console.log(user); delete player.profiles; return player; } @@ -287,6 +289,32 @@ async function fetchProfile(user, profile) { } exports.fetchProfile = fetchProfile; /** + * Fetch a CleanProfile from the uuid + * @param profileUuid A profile name or profile uuid +*/ +async function fetchBasicProfileFromUuid(profileUuid) { + if (exports.profileCache.has(profileUuid)) { + // we have the profile cached, return it :) + if (_1.debug) + console.log('Cache hit! fetchBasicProfileFromUuid', profileUuid); + const profile = exports.profileCache.get(profileUuid); + 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); +} +exports.fetchBasicProfileFromUuid = fetchBasicProfileFromUuid; +/** * 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 diff --git a/build/index.js b/build/index.js index 30d2553..2d7ab25 100644 --- a/build/index.js +++ b/build/index.js @@ -40,7 +40,13 @@ app.get('/player/:user/:profile/leaderboards', async (req, res) => { res.json(await database_1.fetchMemberLeaderboardSpots(req.params.user, req.params.profile)); }); app.get('/leaderboard/:name', async (req, res) => { - res.json(await database_1.fetchMemberLeaderboard(req.params.name)); + try { + res.json(await database_1.fetchLeaderboard(req.params.name)); + } + catch (err) { + console.error(err); + res.json({ 'error': err.toString() }); + } }); app.get('/leaderboards', async (req, res) => { res.json(await database_1.fetchAllLeaderboardsCategorized()); diff --git a/src/cleaners/skyblock/inventory.ts b/src/cleaners/skyblock/inventory.ts index 5d72928..d23bbac 100644 --- a/src/cleaners/skyblock/inventory.ts +++ b/src/cleaners/skyblock/inventory.ts @@ -1,3 +1,4 @@ +// maybe todo?: create a fast replacement for prismarine-nbt import * as nbt from 'prismarine-nbt' function base64decode(base64: string): Buffer { diff --git a/src/cleaners/skyblock/member.ts b/src/cleaners/skyblock/member.ts index fb8132f..5d8d4f9 100644 --- a/src/cleaners/skyblock/member.ts +++ b/src/cleaners/skyblock/member.ts @@ -50,7 +50,7 @@ export async function cleanSkyBlockProfileMemberResponseBasic(member: any, inclu /** Cleans up a member (from skyblock/profile) */ export async function cleanSkyBlockProfileMemberResponse(member, included: Included[] = null): Promise<CleanMember> { // profiles.members[] - const inventoriesIncluded = included == null || included.includes('inventories') + const inventoriesIncluded = included === null || included.includes('inventories') const player = await cached.fetchPlayer(member.uuid) if (!player) return return { diff --git a/src/cleaners/skyblock/profile.ts b/src/cleaners/skyblock/profile.ts index 086cd31..433471b 100644 --- a/src/cleaners/skyblock/profile.ts +++ b/src/cleaners/skyblock/profile.ts @@ -45,7 +45,7 @@ export async function cleanSkyblockProfileResponseLighter(data): Promise<CleanPr /** * 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: any, options?: ApiOptions): Promise<CleanFullProfile> { +export async function cleanSkyblockProfileResponse(data: any, options?: ApiOptions): Promise<CleanFullProfile|CleanProfile> { // We use Promise.all so it can fetch all the users at once instead of waiting for the previous promise to complete const promises: Promise<CleanMember>[] = [] @@ -54,12 +54,23 @@ export async function cleanSkyblockProfileResponse(data: any, options?: ApiOptio memberRaw.uuid = memberUUID promises.push(cleanSkyBlockProfileMemberResponse( memberRaw, - ['stats', options?.mainMemberUuid === memberUUID ? 'inventories' : undefined] + [ + !options?.basic ? 'stats' : undefined, + options?.mainMemberUuid === memberUUID ? 'inventories' : undefined + ] )) } const cleanedMembers: CleanMember[] = (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: CleanMinion[][] = [] for (const member of cleanedMembers) { diff --git a/src/database.ts b/src/database.ts index 983dcb2..c715d52 100644 --- a/src/database.ts +++ b/src/database.ts @@ -3,7 +3,7 @@ */ import { categorizeStat, getStatUnit } from './cleaners/skyblock/stats' -import { CleanFullProfile, CleanProfile } from './cleaners/skyblock/profile' +import { CleanBasicProfile, CleanFullProfile, CleanProfile } from './cleaners/skyblock/profile' import { CleanMember } from './cleaners/skyblock/member' import { Collection, Db, MongoClient } from 'mongodb' import { CleanPlayer } from './cleaners/player' @@ -22,19 +22,32 @@ const recentlyUpdated = new NodeCache({ useClones: false, }) -interface DatabaseLeaderboardItem { +interface DatabaseMemberLeaderboardItem { uuid: string profile: string stats: any last_updated: Date } +interface DatabaseProfileLeaderboardItem { + uuid: string + /** An array of uuids for each player in the profile */ + players: string[] + stats: any + last_updated: Date +} -interface LeaderboardItem { +interface MemberLeaderboardItem { player: CleanPlayer + profileUuid: string + value: number +} +interface ProfileLeaderboardItem { + players: CleanPlayer[] + profileUuid: string value: number } -const cachedRawLeaderboards: Map<string, DatabaseLeaderboardItem[]> = new Map() +const cachedRawLeaderboards: Map<string, (DatabaseMemberLeaderboardItem|DatabaseProfileLeaderboardItem)[]> = new Map() const leaderboardMax = 100 const reversedLeaderboards = [ @@ -45,6 +58,7 @@ const reversedLeaderboards = [ let client: MongoClient let database: Db let memberLeaderboardsCollection: Collection<any> +let profileLeaderboardsCollection: Collection<any> async function connect(): Promise<void> { if (!process.env.db_uri) @@ -54,6 +68,7 @@ async function connect(): Promise<void> { client = await MongoClient.connect(process.env.db_uri, { useNewUrlParser: true, useUnifiedTopology: true }) database = client.db(process.env.db_name) memberLeaderboardsCollection = database.collection('member-leaderboards') + profileLeaderboardsCollection = database.collection('profile-leaderboards') } interface StringNumber { @@ -117,11 +132,20 @@ function getMemberLeaderboardAttributes(member: CleanMember): StringNumber { } } +function getProfileLeaderboardAttributes(profile: CleanFullProfile): StringNumber { + // if you want to add a new leaderboard for member attributes, add it here (and getAllLeaderboardAttributes) + return { + minion_count: profile.minion_count + } +} + export async function fetchAllLeaderboardsCategorized(): Promise<{ [ category: string ]: string[] }> { const memberLeaderboardAttributes: string[] = await fetchAllMemberLeaderboardAttributes() + const profileLeaderboardAttributes: string[] = await fetchAllProfileLeaderboardAttributes() + const categorizedLeaderboards: { [ category: string ]: string[] } = {} - for (const leaderboard of memberLeaderboardAttributes) { + for (const leaderboard of [...memberLeaderboardAttributes, ...profileLeaderboardAttributes]) { const { category } = categorizeStat(leaderboard) if (!categorizedLeaderboards[category]) categorizedLeaderboards[category] = [] @@ -156,7 +180,7 @@ export async function fetchSlayerLeaderboards(): Promise<string[]> { return leaderboardNames } -/** Fetch the names of all the leaderboards */ +/** Fetch the names of all the leaderboards that rank members */ export async function fetchAllMemberLeaderboardAttributes(): Promise<string[]> { 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 @@ -179,6 +203,13 @@ export async function fetchAllMemberLeaderboardAttributes(): Promise<string[]> { ] } +/** Fetch the names of all the leaderboards that rank profiles */ +async function fetchAllProfileLeaderboardAttributes(): Promise<string[]> { + return [ + 'minion_count' + ] +} + function isLeaderboardReversed(name: string): boolean { for (const leaderboardMatch of reversedLeaderboards) { let trailingEnd = leaderboardMatch[0] === '_' @@ -193,9 +224,11 @@ function isLeaderboardReversed(name: string): boolean { return false } -async function fetchMemberLeaderboardRaw(name: string): Promise<DatabaseLeaderboardItem[]> { +async function fetchMemberLeaderboardRaw(name: string): Promise<DatabaseMemberLeaderboardItem[]> { + if (!client) throw Error('Client isn\'t initialized yet') + if (cachedRawLeaderboards.has(name)) - return cachedRawLeaderboards.get(name) + return cachedRawLeaderboards.get(name) as DatabaseMemberLeaderboardItem[] // 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 } @@ -203,7 +236,7 @@ async function fetchMemberLeaderboardRaw(name: string): Promise<DatabaseLeaderbo const sortQuery: any = {} sortQuery[`stats.${name}`] = isLeaderboardReversed(name) ? 1 : -1 - const leaderboardRaw: DatabaseLeaderboardItem[] = await memberLeaderboardsCollection + const leaderboardRaw: DatabaseMemberLeaderboardItem[] = await memberLeaderboardsCollection .find(query) .sort(sortQuery) .limit(leaderboardMax) @@ -213,23 +246,52 @@ async function fetchMemberLeaderboardRaw(name: string): Promise<DatabaseLeaderbo return leaderboardRaw } -interface Leaderboard { +async function fetchProfileLeaderboardRaw(name: string): Promise<DatabaseProfileLeaderboardItem[]> { + if (cachedRawLeaderboards.has(name)) + return cachedRawLeaderboards.get(name) as DatabaseProfileLeaderboardItem[] + // 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: any = {} + sortQuery[`stats.${name}`] = isLeaderboardReversed(name) ? 1 : -1 + + const leaderboardRaw: DatabaseProfileLeaderboardItem[] = await profileLeaderboardsCollection + .find(query) + .sort(sortQuery) + .limit(leaderboardMax) + .toArray() + console.log('leaderboardRaw', leaderboardRaw) + + cachedRawLeaderboards.set(name, leaderboardRaw) + return leaderboardRaw +} + +interface MemberLeaderboard { + name: string + unit?: string + list: MemberLeaderboardItem[] +} + +interface ProfileLeaderboard { name: string unit?: string - list: LeaderboardItem[] + list: ProfileLeaderboardItem[] } + /** Fetch a leaderboard that ranks members, as opposed to profiles */ -export async function fetchMemberLeaderboard(name: string): Promise<Leaderboard> { +export async function fetchMemberLeaderboard(name: string): Promise<MemberLeaderboard> { const leaderboardRaw = await fetchMemberLeaderboardRaw(name) - const fetchLeaderboardPlayer = async(item: DatabaseLeaderboardItem): Promise<LeaderboardItem> => { + const fetchLeaderboardPlayer = async(item: DatabaseMemberLeaderboardItem): Promise<MemberLeaderboardItem> => { return { player: await cached.fetchBasicPlayer(item.uuid), + profileUuid: item.profile, value: item.stats[name] } } - const promises: Promise<LeaderboardItem>[] = [] + const promises: Promise<MemberLeaderboardItem>[] = [] for (const item of leaderboardRaw) { promises.push(fetchLeaderboardPlayer(item)) } @@ -241,6 +303,44 @@ export async function fetchMemberLeaderboard(name: string): Promise<Leaderboard> } } + +/** Fetch a leaderboard that ranks profiles, as opposed to members */ +export async function fetchProfileLeaderboard(name: string): Promise<ProfileLeaderboard> { + const leaderboardRaw = await fetchProfileLeaderboardRaw(name) + + const fetchLeaderboardProfile = async(item: DatabaseProfileLeaderboardItem): Promise<ProfileLeaderboardItem> => { + const players = [] + for (const playerUuid of item.players) + players.push(await cached.fetchBasicPlayer(playerUuid)) + return { + players: players, + profileUuid: item.uuid, + value: item.stats[name] + } + } + const promises: Promise<ProfileLeaderboardItem>[] = [] + 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: string): Promise<MemberLeaderboard|ProfileLeaderboard> { + const profileLeaderboards = await fetchAllProfileLeaderboardAttributes() + console.log(name, profileLeaderboards, profileLeaderboards.includes(name)) + if (profileLeaderboards.includes(name)) { + return await fetchProfileLeaderboard(name) + } else { + return await fetchMemberLeaderboard(name) + } +} + /** Get the leaderboard positions a member is on. This may take a while depending on whether stuff is cached */ export async function fetchMemberLeaderboardSpots(player: string, profile: string) { const fullProfile = await cached.fetchProfile(player, profile) @@ -249,7 +349,7 @@ export async function fetchMemberLeaderboardSpots(player: string, profile: strin // update the leaderboard positions for the member await updateDatabaseMember(fullMember, fullProfile) - const applicableAttributes = await getApplicableAttributes(fullMember) + const applicableAttributes = await getApplicableMemberLeaderboardAttributes(fullMember) const memberLeaderboardSpots = [] @@ -268,8 +368,12 @@ export async function fetchMemberLeaderboardSpots(player: string, profile: strin return memberLeaderboardSpots } -async function getMemberLeaderboardRequirement(name: string): Promise<number> { - const leaderboard = await fetchMemberLeaderboardRaw(name) +async function getLeaderboardRequirement(name: string, leaderboardType: 'member' | 'profile'): Promise<number> { + let leaderboard: DatabaseMemberLeaderboardItem[] | DatabaseProfileLeaderboardItem[] + 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 if (leaderboard.length >= leaderboardMax) @@ -279,12 +383,12 @@ async function getMemberLeaderboardRequirement(name: string): Promise<number> { } /** Get the attributes for the member, but only ones that would put them on the top 100 for leaderboards */ -async function getApplicableAttributes(member: CleanMember): Promise<StringNumber> { +async function getApplicableMemberLeaderboardAttributes(member: CleanMember): Promise<StringNumber> { const leaderboardAttributes = getMemberLeaderboardAttributes(member) const applicableAttributes = {} for (const [ leaderboard, attributeValue ] of Object.entries(leaderboardAttributes)) { - const requirement = await getMemberLeaderboardRequirement(leaderboard) + const requirement = await getLeaderboardRequirement(leaderboard, 'member') const leaderboardReversed = isLeaderboardReversed(leaderboard) if ( (requirement === null) @@ -296,16 +400,44 @@ async function getApplicableAttributes(member: CleanMember): Promise<StringNumbe let leaderboardsCount: number = Object.keys(applicableAttributes).length - const leaderboardsCountRequirement: number = await getMemberLeaderboardRequirement('leaderboards_count') + const leaderboardsCountRequirement: number = await getLeaderboardRequirement('leaderboards_count', 'member') if ( (leaderboardsCountRequirement === null) || (leaderboardsCount > leaderboardsCountRequirement) ) { - // add 1 extra because this attribute also counts :) applicableAttributes['leaderboards_count'] = leaderboardsCount } + 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: CleanFullProfile): Promise<StringNumber> { + const leaderboardAttributes = getProfileLeaderboardAttributes(profile) + const applicableAttributes = {} + + for (const [ leaderboard, attributeValue ] of Object.entries(leaderboardAttributes)) { + const requirement = await getLeaderboardRequirement(leaderboard, 'profile') + const leaderboardReversed = isLeaderboardReversed(leaderboard) + if ( + (requirement === null) + || (leaderboardReversed ? attributeValue < requirement : attributeValue > requirement) + ) { + applicableAttributes[leaderboard] = attributeValue + } + } + + + let leaderboardsCount: number = Object.keys(applicableAttributes).length + const leaderboardsCountRequirement: number = await getLeaderboardRequirement('leaderboards_count', 'member') + + if ( + (leaderboardsCountRequirement === null) + || (leaderboardsCount > leaderboardsCountRequirement) + ) { + applicableAttributes['leaderboards_count'] = leaderboardsCount + } return applicableAttributes } @@ -330,9 +462,9 @@ export async function updateDatabaseMember(member: CleanMember, profile: CleanFu if (debug) console.log('done constants..') - const leaderboardAttributes = await getApplicableAttributes(member) + const leaderboardAttributes = await getApplicableMemberLeaderboardAttributes(member) - if (debug) console.log('done getApplicableAttributes..', leaderboardAttributes, member.username, profile.name) + if (debug) console.log('done getApplicableMemberLeaderboardAttributes..', leaderboardAttributes, member.username, profile.name) await memberLeaderboardsCollection.updateOne( { @@ -368,14 +500,78 @@ export async function updateDatabaseMember(member: CleanMember, profile: CleanFu if (debug) console.log('added member to leaderboards', member.username, leaderboardAttributes) } -const leaderboardUpdateQueue = new Queue({ +/** + * 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: CleanFullProfile): Promise<void> { + if (debug) console.log('updateDatabaseProfile', profile.name) + if (!client) return // the db client hasn't been initialized + + // 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.log('adding profile to leaderboards', profile.name) + + const leaderboardAttributes = await getApplicableProfileLeaderboardAttributes(profile) + + if (debug) console.log('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([{ + last_updated: new Date(), + stats: leaderboardAttributes, + uuid: profile.uuid, + players: profile.members.map(p => p.uuid) + }]) + .sort((a, b) => leaderboardReverse ? a.stats[attributeName] - b.stats[attributeName] : b.stats[attributeName] - a.stats[attributeName]) + .slice(0, 100) + cachedRawLeaderboards.set(attributeName, newRawLeaderboard) + } + + if (debug) console.log('added profile to leaderboards', profile.name, leaderboardAttributes) +} + +const leaderboardUpdateMemberQueue = new Queue({ concurrent: 1, interval: 500 }) +const leaderboardUpdateProfileQueue = new Queue({ + concurrent: 1, + interval: 2000 +}) /** Queue an update for the member's leaderboard data on the server if applicable */ export async function queueUpdateDatabaseMember(member: CleanMember, profile: CleanFullProfile): Promise<void> { - leaderboardUpdateQueue.enqueue(async() => await updateDatabaseMember(member, profile)) + leaderboardUpdateMemberQueue.enqueue(async() => await updateDatabaseMember(member, profile)) +} + +/** Queue an update for the profile's leaderboard data on the server if applicable */ +export async function queueUpdateDatabaseProfile(profile: CleanFullProfile): Promise<void> { + leaderboardUpdateProfileQueue.enqueue(async() => await updateDatabaseProfile(profile)) } @@ -393,7 +589,7 @@ async function removeBadMemberLeaderboardAttributes(): Promise<void> { const unsetValue = {} unsetValue[leaderboard] = '' const filter = {} - const requirement = await getMemberLeaderboardRequirement(leaderboard) + const requirement = await getLeaderboardRequirement(leaderboard, 'member') const leaderboardReversed = isLeaderboardReversed(leaderboard) if (requirement !== null) { filter[`stats.${leaderboard}`] = { diff --git a/src/hypixel.ts b/src/hypixel.ts index 723feea..599a7e1 100644 --- a/src/hypixel.ts +++ b/src/hypixel.ts @@ -9,7 +9,7 @@ import { CleanBasicMember, CleanMemberProfile } from './cleaners/skyblock/member import { cleanSkyblockProfileResponse, CleanProfile, CleanBasicProfile, CleanFullProfile, CleanFullProfileBasicMembers } from './cleaners/skyblock/profile' import { cleanSkyblockProfilesResponse } from './cleaners/skyblock/profiles' import { debug } from '.' -import { queueUpdateDatabaseMember } from './database' +import { queueUpdateDatabaseMember, queueUpdateDatabaseProfile } from './database' export type Included = 'profiles' | 'player' | 'stats' | 'inventories' @@ -25,6 +25,8 @@ export const maxMinion = 11 export interface ApiOptions { mainMemberUuid?: string + /** Only get the most basic information, like uuids and names */ + basic?: boolean } /** Sends an API request to Hypixel and cleans it up. */ @@ -174,7 +176,7 @@ export async function fetchMemberProfile(user: string, profile: string): Promise * @param playerUuid The UUID of the Minecraft player * @param profileUuid The UUID of the Hypixel SkyBlock profile */ -export async function fetchMemberProfileUncached(playerUuid: string, profileUuid: string): Promise<CleanFullProfile> { + export async function fetchMemberProfileUncached(playerUuid: string, profileUuid: string): Promise<CleanFullProfile> { const profile: CleanFullProfile = await sendCleanApiRequest( { path: 'skyblock/profile', @@ -187,6 +189,25 @@ export async function fetchMemberProfileUncached(playerUuid: string, profileUuid // 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: string): Promise<CleanProfile> { + const profile: CleanFullProfile = await sendCleanApiRequest( + { + path: 'skyblock/profile', + args: { profile: profileUuid } + }, + null, + { basic: true } + ) return profile } @@ -208,6 +229,7 @@ export async function fetchMemberProfilesUncached(playerUuid: string): Promise<C for (const member of profile.members) { queueUpdateDatabaseMember(member, profile) } + queueUpdateDatabaseProfile(profile) } return profiles }
\ No newline at end of file diff --git a/src/hypixelCached.ts b/src/hypixelCached.ts index aa3100b..6a84f5e 100644 --- a/src/hypixelCached.ts +++ b/src/hypixelCached.ts @@ -191,6 +191,8 @@ export async function fetchBasicPlayer(user: string): Promise<CleanPlayer> { return basicPlayerCache.get(playerUuid) const player = await fetchPlayer(playerUuid) + if (!player) console.log(user) + delete player.profiles return player } @@ -312,6 +314,32 @@ export async function fetchProfile(user: string, profile: string): Promise<Clean } /** + * Fetch a CleanProfile from the uuid + * @param profileUuid A profile name or profile uuid +*/ +export async function fetchBasicProfileFromUuid(profileUuid: string): Promise<CleanProfile> { + if (profileCache.has(profileUuid)) { + // we have the profile cached, return it :) + if (debug) console.log('Cache hit! fetchBasicProfileFromUuid', profileUuid) + const profile: CleanFullProfile = profileCache.get(profileUuid) + 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 diff --git a/src/index.ts b/src/index.ts index 2c0f1ee..1e641a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { fetchAllLeaderboardsCategorized, fetchMemberLeaderboard, fetchMemberLeaderboardSpots } from './database' +import { fetchAllLeaderboardsCategorized, fetchLeaderboard, fetchMemberLeaderboardSpots } from './database' import { fetchMemberProfile, fetchUser } from './hypixel' import rateLimit from 'express-rate-limit' import express from 'express' @@ -51,9 +51,14 @@ app.get('/player/:user/:profile/leaderboards', async(req, res) => { }) app.get('/leaderboard/:name', async(req, res) => { - res.json( - await fetchMemberLeaderboard(req.params.name) - ) + try { + res.json( + await fetchLeaderboard(req.params.name) + ) + } catch (err) { + console.error(err) + res.json({ 'error': err.toString() }) + } }) app.get('/leaderboards', async(req, res) => { |