diff options
-rw-r--r-- | build/cleaners/skyblock/slayers.js | 30 | ||||
-rw-r--r-- | build/cleaners/skyblock/stats.js | 1 | ||||
-rw-r--r-- | build/constants.js | 47 | ||||
-rw-r--r-- | build/database.js | 44 | ||||
-rw-r--r-- | build/hypixelApi.js | 10 | ||||
-rw-r--r-- | build/mojang.js | 24 | ||||
-rw-r--r-- | src/cleaners/skyblock/slayers.ts | 37 | ||||
-rw-r--r-- | src/cleaners/skyblock/stats.ts | 1 | ||||
-rw-r--r-- | src/constants.ts | 52 | ||||
-rw-r--r-- | src/database.ts | 48 | ||||
-rw-r--r-- | src/hypixelApi.ts | 17 | ||||
-rw-r--r-- | src/mojang.ts | 38 |
12 files changed, 276 insertions, 73 deletions
diff --git a/build/cleaners/skyblock/slayers.js b/build/cleaners/skyblock/slayers.js index d2b9dbe..dbc20fc 100644 --- a/build/cleaners/skyblock/slayers.js +++ b/build/cleaners/skyblock/slayers.js @@ -1,52 +1,64 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.cleanSlayers = void 0; -const slayerLevels = 4; // number of slayer levels, this might be 5 soon +exports.cleanSlayers = exports.slayerLevels = void 0; +exports.slayerLevels = 4; // number of slayer levels, this might be 5 soon const SLAYER_NAMES = { spider: 'tarantula', zombie: 'revenant', wolf: 'sven' }; function cleanSlayers(data) { + var _a, _b; const slayers = []; const slayersDataRaw = data === null || data === void 0 ? void 0 : 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[slayerDataRaw]; - const slayerXp = slayerDataRaw.xp; + const slayerName = SLAYER_NAMES[slayerNameRaw]; + const slayerXp = (_a = slayerDataRaw.xp) !== null && _a !== void 0 ? _a : 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]; + const slayerTierKills = (_b = slayerDataRaw[slayerDataKey]) !== null && _b !== void 0 ? _b : 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) + while (slayerTiers.length < exports.slayerLevels) slayerTiers.push({ tier: slayerTiers.length + 1, kills: 0 }); const slayer = { name: slayerName, + raw_name: slayerNameRaw, tiers: slayerTiers, - xp: slayerXp + xp: slayerXp !== null && slayerXp !== void 0 ? slayerXp : 0, + kills: slayerKills }; slayers.push(slayer); - // add the xp from this slayer to the total xp - totalXp += slayerXp; + // 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 index 5673d99..0eb94c7 100644 --- a/build/cleaners/skyblock/stats.js +++ b/build/cleaners/skyblock/stats.js @@ -10,6 +10,7 @@ const statCategories = { 'mythos': ['mythos_burrows_', 'mythos_kills'], 'collection': ['collection_'], 'skills': ['skill_'], + 'slayer': ['slayer_'], 'misc': null // everything else goes here }; function categorizeStat(statNameRaw) { diff --git a/build/constants.js b/build/constants.js index 6344a12..96ae2e7 100644 --- a/build/constants.js +++ b/build/constants.js @@ -6,7 +6,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.addZones = exports.fetchZones = exports.addSkills = exports.fetchSkills = exports.addCollections = exports.fetchCollections = exports.addStats = exports.fetchStats = exports.addJSONConstants = void 0; +exports.addSlayers = exports.fetchSlayers = exports.addZones = exports.fetchZones = exports.addSkills = exports.fetchSkills = exports.addCollections = exports.fetchCollections = exports.addStats = exports.fetchStats = exports.addJSONConstants = void 0; const node_fetch_1 = __importDefault(require("node-fetch")); const https_1 = require("https"); const node_cache_1 = __importDefault(require("node-cache")); @@ -30,14 +30,21 @@ const queue = new queue_promise_1.default({ * @param json The JSON body, only applicable for some types of methods */ async function fetchGithubApi(method, route, headers, json) { - return await node_fetch_1.default(githubApiBase + route, { - agent: () => httpsAgent, - body: json ? JSON.stringify(json) : null, - method, - headers: Object.assign({ - 'Authorization': `token ${process.env.github_token}` - }, headers), - }); + try { + return await node_fetch_1.default(githubApiBase + route, { + agent: () => httpsAgent, + body: json ? JSON.stringify(json) : null, + method, + headers: Object.assign({ + 'Authorization': `token ${process.env.github_token}` + }, headers), + }); + } + 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 node_cache_1.default({ @@ -95,7 +102,7 @@ async function fetchJSONConstant(filename) { } } /** Add stats to skyblock-constants. This has caching so it's fine to call many times */ -async function addJSONConstants(filename, addingValues, units = 'stats') { +async function addJSONConstants(filename, addingValues, unit = 'stat') { if (addingValues.length === 0) return; // no stats provided, just return queue.enqueue(async () => { @@ -119,7 +126,7 @@ async function addJSONConstants(filename, addingValues, units = 'stats') { // there's not actually any new stats, just return if (newStats.length === 0) return; - const commitMessage = newStats.length >= 2 ? `Add ${newStats.length} new ${units}` : `Add '${newStats[0]}'`; + const commitMessage = newStats.length >= 2 ? `Add ${newStats.length} new ${unit}s` : `Add '${newStats[0]}' ${unit}`; await editFile(file, commitMessage, JSON.stringify(updatedStats, null, 2)); }); } @@ -131,7 +138,7 @@ async function fetchStats() { exports.fetchStats = fetchStats; /** Add stats to skyblock-constants. This has caching so it's fine to call many times */ async function addStats(addingStats) { - await addJSONConstants('stats.json', addingStats, 'stats'); + await addJSONConstants('stats.json', addingStats, 'stat'); } exports.addStats = addStats; /** Fetch all the known SkyBlock collections as an array of strings */ @@ -141,7 +148,7 @@ async function fetchCollections() { exports.fetchCollections = fetchCollections; /** Add collections to skyblock-constants. This has caching so it's fine to call many times */ async function addCollections(addingCollections) { - await addJSONConstants('collections.json', addingCollections, 'collections'); + await addJSONConstants('collections.json', addingCollections, 'collection'); } exports.addCollections = addCollections; /** Fetch all the known SkyBlock collections as an array of strings */ @@ -151,7 +158,7 @@ async function fetchSkills() { exports.fetchSkills = fetchSkills; /** Add skills to skyblock-constants. This has caching so it's fine to call many times */ async function addSkills(addingSkills) { - await addJSONConstants('skills.json', addingSkills, 'skills'); + await addJSONConstants('skills.json', addingSkills, 'skill'); } exports.addSkills = addSkills; /** Fetch all the known SkyBlock collections as an array of strings */ @@ -161,6 +168,16 @@ async function fetchZones() { exports.fetchZones = fetchZones; /** Add skills to skyblock-constants. This has caching so it's fine to call many times */ async function addZones(addingZones) { - await addJSONConstants('zones.json', addingZones, 'zones'); + await addJSONConstants('zones.json', addingZones, 'zone'); } exports.addZones = addZones; +/** Fetch all the known SkyBlock slayer names as an array of strings */ +async function fetchSlayers() { + return await fetchJSONConstant('slayers.json'); +} +exports.fetchSlayers = fetchSlayers; +/** Add skills to skyblock-constants. This has caching so it's fine to call many times */ +async function addSlayers(addingSlayers) { + await addJSONConstants('slayers.json', addingSlayers, 'slayer'); +} +exports.addSlayers = addSlayers; diff --git a/build/database.js b/build/database.js index b40d30c..967828e 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.fetchAllLeaderboardsCategorized = void 0; +exports.queueUpdateDatabaseMember = exports.updateDatabaseMember = exports.fetchMemberLeaderboardSpots = 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")); @@ -34,6 +34,7 @@ const util_1 = require("./util"); const node_cache_1 = __importDefault(require("node-cache")); const queue_promise_1 = __importDefault(require("queue-promise")); const _1 = require("."); +const slayers_1 = require("./cleaners/skyblock/slayers"); // don't update the user for 3 minutes const recentlyUpdated = new node_cache_1.default({ stdTTL: 60 * 3, @@ -74,6 +75,20 @@ function getMemberSkillAttributes(member) { } 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.name}_total_xp`] = slayer.xp; + slayerAttributes[`slayer_${slayer.name}_total_kills`] = slayer.kills; + for (const tier of slayer.tiers) { + slayerAttributes[`slayer_${slayer.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 { @@ -83,6 +98,8 @@ function getMemberLeaderboardAttributes(member) { ...getMemberCollectionAttributes(member), // skill leaderboards ...getMemberSkillAttributes(member), + // slayer leaderboards + ...getMemberSlayerAttributes(member), fairy_souls: member.fairy_souls.total, first_join: member.first_join, purse: member.purse, @@ -105,6 +122,24 @@ async function fetchAllLeaderboardsCategorized() { return categorizedLeaderboards; } exports.fetchAllLeaderboardsCategorized = fetchAllLeaderboardsCategorized; +/** Fetch the raw names for the slayer leaderboards */ +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 <= slayers_1.slayerLevels; slayerTier++) { + leaderboardNames.push(`slayer_${slayerNameRaw}_${slayerTier}_kills`); + } + } + return leaderboardNames; +} +exports.fetchSlayerLeaderboards = fetchSlayerLeaderboards; /** Fetch the names of all the leaderboards */ async function fetchAllMemberLeaderboardAttributes() { return [ @@ -114,6 +149,8 @@ async function fetchAllMemberLeaderboardAttributes() { ...(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', @@ -231,6 +268,7 @@ async function updateDatabaseMember(member, profile) { 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 (_1.debug) console.log('done constants..'); const leaderboardAttributes = await getApplicableAttributes(member); @@ -265,13 +303,13 @@ async function updateDatabaseMember(member, profile) { console.log('added member to leaderboards', member.username, leaderboardAttributes); } exports.updateDatabaseMember = updateDatabaseMember; -const queue = new queue_promise_1.default({ +const leaderboardUpdateQueue = new queue_promise_1.default({ concurrent: 1, interval: 500 }); /** Queue an update for the member's leaderboard data on the server if applicable */ async function queueUpdateDatabaseMember(member, profile) { - queue.enqueue(async () => await updateDatabaseMember(member, profile)); + leaderboardUpdateQueue.enqueue(async () => await updateDatabaseMember(member, profile)); } exports.queueUpdateDatabaseMember = queueUpdateDatabaseMember; /** diff --git a/build/hypixelApi.js b/build/hypixelApi.js index ca2526f..2b3a601 100644 --- a/build/hypixelApi.js +++ b/build/hypixelApi.js @@ -51,7 +51,15 @@ async function sendApiRequest({ path, key, args }) { args.key = key; // Construct a url from the base api url, path, and arguments const fetchUrl = baseHypixelAPI + '/' + path + '?' + util_1.jsonToQuery(args); - const fetchResponse = await node_fetch_1.default(fetchUrl, { agent: () => httpsAgent }); + let fetchResponse; + try { + fetchResponse = await node_fetch_1.default(fetchUrl, { agent: () => httpsAgent }); + } + 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 }); + } if (fetchResponse.headers['ratelimit-limit']) // remember how many uses it has apiKeyUsage[key] = { diff --git a/build/mojang.js b/build/mojang.js index f18875d..9641422 100644 --- a/build/mojang.js +++ b/build/mojang.js @@ -18,9 +18,17 @@ const httpsAgent = new https_1.Agent({ * Get mojang api data from the session server */ async function profileFromUuid(uuid) { - const fetchResponse = await node_fetch_1.default( - // using mojang directly is faster than ashcon lol, also mojang removed the ratelimits from here - `https://sessionserver.mojang.com/session/minecraft/profile/${util_1.undashUuid(uuid)}`, { agent: () => httpsAgent }); + let fetchResponse; + try { + fetchResponse = await node_fetch_1.default( + // using mojang directly is faster than ashcon lol, also mojang removed the ratelimits from here + `https://sessionserver.mojang.com/session/minecraft/profile/${util_1.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 data; try { data = await fetchResponse.json(); @@ -37,7 +45,15 @@ async function profileFromUuid(uuid) { exports.profileFromUuid = profileFromUuid; async function profileFromUsername(username) { // since we don't care about anything other than the uuid, we can use /uuid/ instead of /user/ - const fetchResponse = await node_fetch_1.default(`https://api.mojang.com/users/profiles/minecraft/${username}`, { agent: () => httpsAgent }); + let fetchResponse; + try { + fetchResponse = await node_fetch_1.default(`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; try { data = await fetchResponse.json(); diff --git a/src/cleaners/skyblock/slayers.ts b/src/cleaners/skyblock/slayers.ts index dd3881b..2026496 100644 --- a/src/cleaners/skyblock/slayers.ts +++ b/src/cleaners/skyblock/slayers.ts @@ -1,5 +1,4 @@ - -const slayerLevels = 4 // number of slayer levels, this might be 5 soon +export const slayerLevels = 4 // number of slayer levels, this might be 5 soon const SLAYER_NAMES = { spider: 'tarantula', @@ -17,12 +16,15 @@ interface SlayerTier { export interface Slayer { name: SlayerName + raw_name: string xp: number + kills: number tiers: SlayerTier[] } export interface SlayerData { xp: number + kills: number bosses: Slayer[] } @@ -31,27 +33,34 @@ export function cleanSlayers(data: any): SlayerData { const slayersDataRaw = data?.slayer_bosses - let totalXp = 0 + let totalXp: number = 0 + let totalKills: number = 0 for (const slayerNameRaw in slayersDataRaw) { const slayerDataRaw = slayersDataRaw[slayerNameRaw] // convert name provided by api (spider) to the real name (tarantula) - const slayerName: SlayerName = SLAYER_NAMES[slayerDataRaw] + const slayerName: SlayerName = SLAYER_NAMES[slayerNameRaw] - const slayerXp: number = slayerDataRaw.xp + const slayerXp: number = slayerDataRaw.xp ?? 0 + let slayerKills: number = 0 const slayerTiers: SlayerTier[] = [] + 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] + 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 } } @@ -64,15 +73,25 @@ export function cleanSlayers(data: any): SlayerData { const slayer: Slayer = { name: slayerName, + raw_name: slayerNameRaw, tiers: slayerTiers, - xp: slayerXp + xp: slayerXp ?? 0, + kills: slayerKills } + slayers.push(slayer) - // add the xp from this slayer to the total xp - totalXp += slayerXp + + // 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/src/cleaners/skyblock/stats.ts b/src/cleaners/skyblock/stats.ts index efb79bd..218e034 100644 --- a/src/cleaners/skyblock/stats.ts +++ b/src/cleaners/skyblock/stats.ts @@ -8,6 +8,7 @@ const statCategories: { [ key: string ]: string[] | null } = { // sorted in orde 'collection': ['collection_'], 'skills': ['skill_'], + 'slayer': ['slayer_'], 'misc': null // everything else goes here } diff --git a/src/constants.ts b/src/constants.ts index 760dcb1..5f20147 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -30,17 +30,23 @@ const queue = new Queue({ * @param json The JSON body, only applicable for some types of methods */ async function fetchGithubApi(method: string, route: string, headers?: any, json?: any): Promise<nodeFetch.Response> { - return await fetch( - githubApiBase + route, - { - agent: () => httpsAgent, - body: json ? JSON.stringify(json) : null, - method, - headers: Object.assign({ - 'Authorization': `token ${process.env.github_token}` - }, headers), - } - ) + try { + return await fetch( + githubApiBase + route, + { + agent: () => httpsAgent, + body: json ? JSON.stringify(json) : null, + method, + headers: Object.assign({ + 'Authorization': `token ${process.env.github_token}` + }, headers), + } + ) + } 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) + } } interface GithubFile { @@ -73,6 +79,7 @@ async function fetchFile(path: string): Promise<GithubFile> { }, ) const data = await r.json() + const file = { path: data.path, content: Buffer.from(data.content, data.encoding).toString(), @@ -119,7 +126,7 @@ async function fetchJSONConstant(filename: string): Promise<string[]> { } /** Add stats to skyblock-constants. This has caching so it's fine to call many times */ -export async function addJSONConstants(filename: string, addingValues: string[], units: string='stats'): Promise<void> { +export async function addJSONConstants(filename: string, addingValues: string[], unit: string='stat'): Promise<void> { if (addingValues.length === 0) return // no stats provided, just return queue.enqueue(async() => { @@ -143,7 +150,7 @@ export async function addJSONConstants(filename: string, addingValues: string[], // there's not actually any new stats, just return if (newStats.length === 0) return - const commitMessage = newStats.length >= 2 ? `Add ${newStats.length} new ${units}` : `Add '${newStats[0]}'` + const commitMessage = newStats.length >= 2 ? `Add ${newStats.length} new ${unit}s` : `Add '${newStats[0]}' ${unit}` await editFile(file, commitMessage, JSON.stringify(updatedStats, null, 2)) }) } @@ -156,7 +163,7 @@ export async function fetchStats(): Promise<string[]> { /** Add stats to skyblock-constants. This has caching so it's fine to call many times */ export async function addStats(addingStats: string[]): Promise<void> { - await addJSONConstants('stats.json', addingStats, 'stats') + await addJSONConstants('stats.json', addingStats, 'stat') } /** Fetch all the known SkyBlock collections as an array of strings */ @@ -166,7 +173,7 @@ export async function fetchCollections(): Promise<string[]> { /** Add collections to skyblock-constants. This has caching so it's fine to call many times */ export async function addCollections(addingCollections: string[]): Promise<void> { - await addJSONConstants('collections.json', addingCollections, 'collections') + await addJSONConstants('collections.json', addingCollections, 'collection') } /** Fetch all the known SkyBlock collections as an array of strings */ @@ -176,7 +183,7 @@ export async function fetchSkills(): Promise<string[]> { /** Add skills to skyblock-constants. This has caching so it's fine to call many times */ export async function addSkills(addingSkills: string[]): Promise<void> { - await addJSONConstants('skills.json', addingSkills, 'skills') + await addJSONConstants('skills.json', addingSkills, 'skill') } /** Fetch all the known SkyBlock collections as an array of strings */ @@ -186,5 +193,16 @@ export async function fetchZones(): Promise<string[]> { /** Add skills to skyblock-constants. This has caching so it's fine to call many times */ export async function addZones(addingZones: string[]): Promise<void> { - await addJSONConstants('zones.json', addingZones, 'zones') + await addJSONConstants('zones.json', addingZones, 'zone') +} + + +/** Fetch all the known SkyBlock slayer names as an array of strings */ +export async function fetchSlayers(): Promise<string[]> { + return await fetchJSONConstant('slayers.json') +} + +/** Add skills to skyblock-constants. This has caching so it's fine to call many times */ +export async function addSlayers(addingSlayers: string[]): Promise<void> { + await addJSONConstants('slayers.json', addingSlayers, 'slayer') } diff --git a/src/database.ts b/src/database.ts index 9e81f5b..3f44b39 100644 --- a/src/database.ts +++ b/src/database.ts @@ -13,6 +13,7 @@ import { shuffle, sleep } from './util' import NodeCache from 'node-cache' import Queue from 'queue-promise' import { debug } from '.' +import { slayerLevels } from './cleaners/skyblock/slayers' // don't update the user for 3 minutes const recentlyUpdated = new NodeCache({ @@ -77,6 +78,23 @@ function getMemberSkillAttributes(member: CleanMember): StringNumber { return skillAttributes } +function getMemberSlayerAttributes(member: CleanMember): StringNumber { + const slayerAttributes: StringNumber = { + slayer_total_xp: member.slayers.xp, + slayer_total_kills: member.slayers.kills, + } + + for (const slayer of member.slayers.bosses) { + slayerAttributes[`slayer_${slayer.name}_total_xp`] = slayer.xp + slayerAttributes[`slayer_${slayer.name}_total_kills`] = slayer.kills + for (const tier of slayer.tiers) { + slayerAttributes[`slayer_${slayer.name}_${tier.tier}_kills`] = tier.kills + } + } + + return slayerAttributes +} + function getMemberLeaderboardAttributes(member: CleanMember): StringNumber { // if you want to add a new leaderboard for member attributes, add it here (and getAllLeaderboardAttributes) return { @@ -89,6 +107,9 @@ function getMemberLeaderboardAttributes(member: CleanMember): StringNumber { // skill leaderboards ...getMemberSkillAttributes(member), + // slayer leaderboards + ...getMemberSlayerAttributes(member), + fairy_souls: member.fairy_souls.total, first_join: member.first_join, purse: member.purse, @@ -115,6 +136,25 @@ export async function fetchAllLeaderboardsCategorized(): Promise<{ [ category: s return categorizedLeaderboards } +/** Fetch the raw names for the slayer leaderboards */ +export async function fetchSlayerLeaderboards(): Promise<string[]> { + const rawSlayerNames = await constants.fetchSlayers() + let leaderboardNames: string[] = [ + '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 */ export async function fetchAllMemberLeaderboardAttributes(): Promise<string[]> { @@ -128,6 +168,9 @@ export async function fetchAllMemberLeaderboardAttributes(): Promise<string[]> { // skill leaderboards ...(await constants.fetchSkills()).map(value => `skill_${value}`), + // slayer leaderboards + ...await fetchSlayerLeaderboards(), + 'fairy_souls', 'first_join', 'purse', @@ -265,6 +308,7 @@ export async function updateDatabaseMember(member: CleanMember, profile: CleanFu 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.log('done constants..') @@ -306,14 +350,14 @@ export async function updateDatabaseMember(member: CleanMember, profile: CleanFu if (debug) console.log('added member to leaderboards', member.username, leaderboardAttributes) } -const queue = new Queue({ +const leaderboardUpdateQueue = new Queue({ concurrent: 1, interval: 500 }) /** Queue an update for the member's leaderboard data on the server if applicable */ export async function queueUpdateDatabaseMember(member: CleanMember, profile: CleanFullProfile): Promise<void> { - queue.enqueue(async() => await updateDatabaseMember(member, profile)) + leaderboardUpdateQueue.enqueue(async() => await updateDatabaseMember(member, profile)) } diff --git a/src/hypixelApi.ts b/src/hypixelApi.ts index 5c4bb2b..8541089 100644 --- a/src/hypixelApi.ts +++ b/src/hypixelApi.ts @@ -2,6 +2,7 @@ * Fetch the raw Hypixel API */ import fetch from 'node-fetch' +import * as nodeFetch from 'node-fetch' import { jsonToQuery, shuffle } from './util' import { Agent } from 'https' @@ -136,10 +137,18 @@ export async function sendApiRequest({ path, key, args }): Promise<HypixelRespon // Construct a url from the base api url, path, and arguments const fetchUrl = baseHypixelAPI + '/' + path + '?' + jsonToQuery(args) - const fetchResponse = await fetch( - fetchUrl, - { agent: () => httpsAgent } - ) + let fetchResponse: nodeFetch.Response + + try { + fetchResponse = await fetch( + fetchUrl, + { agent: () => httpsAgent } + ) + } 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 }) + } if (fetchResponse.headers['ratelimit-limit']) // remember how many uses it has diff --git a/src/mojang.ts b/src/mojang.ts index ce5b0bb..829de7f 100644 --- a/src/mojang.ts +++ b/src/mojang.ts @@ -3,6 +3,7 @@ */ import fetch from 'node-fetch' +import * as nodeFetch from 'node-fetch' import { Agent } from 'https' import { isUuid, undashUuid } from './util' @@ -22,11 +23,20 @@ interface MojangApiResponse { * Get mojang api data from the session server */ export async function profileFromUuid(uuid: string): Promise<MojangApiResponse> { - const 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 } - ) + let fetchResponse: nodeFetch.Response + + 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 data try { data = await fetchResponse.json() @@ -43,10 +53,20 @@ export async function profileFromUuid(uuid: string): Promise<MojangApiResponse> export async function profileFromUsername(username: string): Promise<MojangApiResponse> { // since we don't care about anything other than the uuid, we can use /uuid/ instead of /user/ - const fetchResponse = await fetch( - `https://api.mojang.com/users/profiles/minecraft/${username}`, - { agent: () => httpsAgent } - ) + + let fetchResponse: nodeFetch.Response + + 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 try { data = await fetchResponse.json() |