aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/cleaners/player.js19
-rw-r--r--build/cleaners/rank.js55
-rw-r--r--build/cleaners/skyblock/minions.js63
-rw-r--r--build/cleaners/skyblock/stats.js66
-rw-r--r--build/cleaners/socialmedia.js10
-rw-r--r--build/hypixel.js212
-rw-r--r--build/hypixelApi.js66
-rw-r--r--build/hypixelCached.js227
-rw-r--r--build/index.js18
-rw-r--r--build/mojang.js40
-rw-r--r--build/util.js78
-rw-r--r--package.json2
12 files changed, 855 insertions, 1 deletions
diff --git a/build/cleaners/player.js b/build/cleaners/player.js
new file mode 100644
index 0000000..25ae1f2
--- /dev/null
+++ b/build/cleaners/player.js
@@ -0,0 +1,19 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.cleanPlayerResponse = void 0;
+const hypixel_1 = require("../hypixel");
+const socialmedia_1 = require("./socialmedia");
+const rank_1 = require("./rank");
+const util_1 = require("../util");
+async function cleanPlayerResponse(data) {
+ // Cleans up a 'player' api response
+ console.log('cleanPlayerResponse', data.stats.SkyBlock.profiles);
+ return {
+ uuid: util_1.undashUuid(data.uuid),
+ username: data.displayname,
+ rank: rank_1.parseRank(data),
+ socials: socialmedia_1.parseSocialMedia(data.socialMedia),
+ profiles: hypixel_1.cleanPlayerSkyblockProfiles(data.stats.SkyBlock.profiles)
+ };
+}
+exports.cleanPlayerResponse = cleanPlayerResponse;
diff --git a/build/cleaners/rank.js b/build/cleaners/rank.js
new file mode 100644
index 0000000..875d2db
--- /dev/null
+++ b/build/cleaners/rank.js
@@ -0,0 +1,55 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.parseRank = void 0;
+const util_1 = require("../util");
+const rankColors = {
+ 'NONE': '7',
+ 'VIP': 'a',
+ 'VIP+': 'a',
+ 'MVP': 'b',
+ 'MVP+': 'b',
+ 'MVP++': '6',
+ 'YOUTUBE': 'c',
+ 'HELPER': '9',
+ 'MODERATOR': '2',
+ 'ADMIN': 'c'
+};
+/** Response cleaning (reformatting to be nicer) */
+function parseRank({ packageRank, newPackageRank, monthlyPackageRank, rankPlusColor, rank, prefix }) {
+ let name;
+ let color;
+ let colored;
+ if (prefix) { // derive values from prefix
+ colored = prefix;
+ color = util_1.minecraftColorCodes[colored.match(/§./)[0][1]];
+ name = colored.replace(/§./g, '').replace(/[\[\]]/g, '');
+ }
+ else {
+ name = rank
+ || newPackageRank.replace('_PLUS', '+')
+ || packageRank.replace('_PLUS', '+')
+ || monthlyPackageRank;
+ // MVP++ is called Superstar for some reason
+ if (name === 'SUPERSTAR')
+ name = 'MVP++';
+ // YouTube rank is called YouTuber, change this to the proper name
+ else if (name === 'YOUTUBER')
+ name = 'YOUTUBE';
+ const plusColor = util_1.colorCodeFromName(rankPlusColor);
+ color = util_1.minecraftColorCodes[rankColors[name]];
+ const rankColorPrefix = rankColors[name] ? '§' + rankColors[name] : '';
+ const nameWithoutPlus = name.split('+')[0];
+ const plusesInName = '+'.repeat(name.split('+').length - 1);
+ console.log(plusColor, nameWithoutPlus, plusesInName);
+ if (plusColor && plusesInName.length >= 1)
+ colored = `${rankColorPrefix}[${nameWithoutPlus}§${plusColor}${plusesInName}${rankColorPrefix}]`;
+ else
+ colored = `${rankColorPrefix}[${name}]`;
+ }
+ return {
+ name,
+ color,
+ colored
+ };
+}
+exports.parseRank = parseRank;
diff --git a/build/cleaners/skyblock/minions.js b/build/cleaners/skyblock/minions.js
new file mode 100644
index 0000000..8ffa9c1
--- /dev/null
+++ b/build/cleaners/skyblock/minions.js
@@ -0,0 +1,63 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.combineMinionArrays = exports.cleanMinions = void 0;
+const hypixel_1 = require("../../hypixel");
+/**
+ * Clean the minions provided by Hypixel
+ * @param minionsRaw The minion data provided by the Hypixel API
+ */
+function cleanMinions(minionsRaw) {
+ const minions = [];
+ for (const minionRaw of minionsRaw ?? []) {
+ // 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(hypixel_1.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;
+ }
+ return minions;
+}
+exports.cleanMinions = cleanMinions;
+/**
+ * Combine multiple arrays of minions into one, useful when getting the minions for members
+ * @param minions An array of arrays of minions
+ */
+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(null);
+ for (let i = 0; i < minion.levels.length; i++) {
+ if (minion.levels[i])
+ matchingMinionReference.levels[i] = true;
+ }
+ }
+ }
+ }
+ return resultMinions;
+}
+exports.combineMinionArrays = combineMinionArrays;
diff --git a/build/cleaners/skyblock/stats.js b/build/cleaners/skyblock/stats.js
new file mode 100644
index 0000000..81f5544
--- /dev/null
+++ b/build/cleaners/skyblock/stats.js
@@ -0,0 +1,66 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.cleanProfileStats = void 0;
+const statCategories = {
+ 'deaths': ['deaths_', 'deaths'],
+ 'kills': ['kills_', 'kills'],
+ 'fishing': ['items_fished_', 'items_fished'],
+ 'auctions': ['auctions_'],
+ 'races': ['_best_time'],
+ 'misc': null // everything else goes here
+};
+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, 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
+ };
+}
+function cleanProfileStats(statsRaw) {
+ // TODO: add type for statsRaw (probably in hypixelApi.ts since its coming from there)
+ const stats = {};
+ for (let statNameRaw in statsRaw) {
+ let { category: statCategory, name: statName } = categorizeStat(statNameRaw);
+ if (!stats[statCategory])
+ stats[statCategory] = {};
+ stats[statCategory][statName || 'total'] = statsRaw[statNameRaw];
+ }
+ return stats;
+}
+exports.cleanProfileStats = cleanProfileStats;
diff --git a/build/cleaners/socialmedia.js b/build/cleaners/socialmedia.js
new file mode 100644
index 0000000..fb0bcfd
--- /dev/null
+++ b/build/cleaners/socialmedia.js
@@ -0,0 +1,10 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.parseSocialMedia = void 0;
+function parseSocialMedia(socialMedia) {
+ return {
+ discord: socialMedia?.links?.DISCORD || null,
+ forums: socialMedia?.links?.HYPIXEL || null
+ };
+}
+exports.parseSocialMedia = parseSocialMedia;
diff --git a/build/hypixel.js b/build/hypixel.js
new file mode 100644
index 0000000..0ad63f8
--- /dev/null
+++ b/build/hypixel.js
@@ -0,0 +1,212 @@
+"use strict";
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+ __setModuleDefault(result, mod);
+ return result;
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.fetchMemberProfile = exports.fetchUser = exports.cleanPlayerSkyblockProfiles = exports.sendCleanApiRequest = exports.maxMinion = exports.saveInterval = void 0;
+const minions_1 = require("./cleaners/skyblock/minions");
+const stats_1 = require("./cleaners/skyblock/stats");
+const player_1 = require("./cleaners/player");
+const hypixelApi_1 = require("./hypixelApi");
+const cached = __importStar(require("./hypixelCached"));
+// the interval at which the "last_save" parameter updates in the hypixel api, this is 3 minutes
+exports.saveInterval = 60 * 3 * 1000;
+// the highest level a minion can be
+exports.maxMinion = 11;
+/**
+ * Send a request to api.hypixel.net using a random key, clean it up to be more useable, and return it
+ */
+async function sendCleanApiRequest({ path, args }, included, cleaned = true) {
+ const key = await hypixelApi_1.chooseApiKey();
+ const rawResponse = await hypixelApi_1.sendApiRequest({ path, key, args });
+ if (rawResponse.throttled) {
+ // if it's throttled, wait a second and try again
+ console.log('throttled :/');
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ return await sendCleanApiRequest({ path, args }, included, cleaned);
+ }
+ if (cleaned) {
+ // if it needs to clean the response, call cleanResponse
+ return await cleanResponse({ path, data: rawResponse }, included = included);
+ }
+ else {
+ // this is provided in case the caller wants to do the cleaning itself
+ // used in skyblock/profile, as cleaning the entire profile would use too much cpu
+ return rawResponse;
+ }
+}
+exports.sendCleanApiRequest = sendCleanApiRequest;
+async function cleanSkyBlockProfileMemberResponse(member, included = null) {
+ // Cleans up a member (from skyblock/profile)
+ // profiles.members[]
+ const statsIncluded = included == null || included.includes('stats');
+ return {
+ uuid: member.uuid,
+ username: await cached.usernameFromUser(member.uuid),
+ last_save: member.last_save,
+ first_join: member.first_join,
+ // last_death: ??? idk how this is formatted,
+ stats: statsIncluded ? stats_1.cleanProfileStats(member.stats) : undefined,
+ minions: statsIncluded ? minions_1.cleanMinions(member.crafted_generators) : undefined,
+ };
+}
+/** Return a `CleanProfile` instead of a `CleanFullProfile`, useful when we need to get members but don't want to waste much ram */
+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(cleanSkyBlockProfileMemberResponse(memberRaw, []));
+ }
+ const cleanedMembers = await Promise.all(promises);
+ return {
+ uuid: data.profile_id,
+ name: data.cute_name,
+ members: cleanedMembers,
+ };
+}
+/** This function is very costly and shouldn't be called often. Use cleanSkyblockProfileResponseLighter if you don't need all the data */
+async function cleanSkyblockProfileResponse(data) {
+ const cleanedMembers = [];
+ for (const memberUUID in data.members) {
+ const memberRaw = data.members[memberUUID];
+ memberRaw.uuid = memberUUID;
+ const member = await cleanSkyBlockProfileMemberResponse(memberRaw, ['stats']);
+ cleanedMembers.push(member);
+ }
+ const memberMinions = [];
+ for (const member of cleanedMembers) {
+ memberMinions.push(member.minions);
+ }
+ const minions = minions_1.combineMinionArrays(memberMinions);
+ // return more detailed info
+ return {
+ uuid: data.profile_id,
+ name: data.cute_name,
+ members: cleanedMembers,
+ bank: {
+ balance: data?.banking?.balance ?? 0,
+ // TODO: make transactions good
+ history: data?.banking?.transactions ?? []
+ },
+ minions
+ };
+}
+function cleanPlayerSkyblockProfiles(rawProfiles) {
+ let profiles = [];
+ for (const profile of Object.values(rawProfiles)) {
+ profiles.push({
+ uuid: profile.profile_id,
+ name: profile.cute_name
+ });
+ }
+ console.log('cleanPlayerSkyblockProfiles', profiles);
+ return profiles;
+}
+exports.cleanPlayerSkyblockProfiles = cleanPlayerSkyblockProfiles;
+/** Convert an array of raw profiles into clean profiles */
+async function cleanSkyblockProfilesResponse(data) {
+ const cleanedProfiles = [];
+ for (const profile of data) {
+ let cleanedProfile = await cleanSkyblockProfileResponseLighter(profile);
+ cleanedProfiles.push(cleanedProfile);
+ }
+ return cleanedProfiles;
+}
+async function cleanResponse({ path, data }, included) {
+ // Cleans up an api response
+ switch (path) {
+ case 'player': return await player_1.cleanPlayerResponse(data.player);
+ case 'skyblock/profile': return await cleanSkyblockProfileResponse(data.profile);
+ 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
+ */
+async function fetchUser({ user, uuid, username }, included = ['player']) {
+ if (!uuid) {
+ // If the uuid isn't provided, get it
+ uuid = await cached.uuidFromUser(user || username);
+ }
+ const includePlayers = included.includes('player');
+ const includeProfiles = included.includes('profiles');
+ let profilesData;
+ let basicProfilesData;
+ let playerData;
+ if (includePlayers) {
+ playerData = await cached.fetchPlayer(uuid);
+ // if not including profiles, include lightweight profiles just in case
+ if (!includeProfiles)
+ basicProfilesData = playerData.profiles;
+ playerData.profiles = undefined;
+ }
+ if (includeProfiles) {
+ profilesData = await cached.fetchSkyblockProfiles(uuid);
+ }
+ let activeProfile = null;
+ let lastOnline = 0;
+ if (includeProfiles) {
+ for (const profile of profilesData) {
+ const member = profile.members.find(member => member.uuid === uuid);
+ if (member.last_save > lastOnline) {
+ lastOnline = member.last_save;
+ activeProfile = profile;
+ }
+ }
+ }
+ return {
+ player: playerData ?? null,
+ profiles: profilesData ?? basicProfilesData,
+ activeProfile: includeProfiles ? activeProfile?.uuid : undefined,
+ online: includeProfiles ? lastOnline > (Date.now() - exports.saveInterval) : undefined
+ };
+}
+exports.fetchUser = fetchUser;
+/**
+ * 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
+ */
+async function fetchMemberProfile(user, profile) {
+ const playerUuid = await cached.uuidFromUser(user);
+ const profileUuid = await cached.fetchProfileUuid(user, profile);
+ const player = await cached.fetchPlayer(playerUuid);
+ const cleanProfile = await cached.fetchProfile(playerUuid, profileUuid);
+ const member = cleanProfile.members.find(m => m.uuid === playerUuid);
+ return {
+ member: {
+ profileName: cleanProfile.name,
+ first_join: member.first_join,
+ last_save: member.last_save,
+ // add all other data relating to the hypixel player, such as username, rank, etc
+ ...player
+ },
+ profile: {
+ minions: cleanProfile.minions
+ }
+ };
+}
+exports.fetchMemberProfile = fetchMemberProfile;
diff --git a/build/hypixelApi.js b/build/hypixelApi.js
new file mode 100644
index 0000000..5cbd829
--- /dev/null
+++ b/build/hypixelApi.js
@@ -0,0 +1,66 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.sendApiRequest = exports.chooseApiKey = void 0;
+const node_fetch_1 = __importDefault(require("node-fetch"));
+const util_1 = require("./util");
+const https_1 = require("https");
+require('dotenv').config();
+// We need to create an agent to prevent memory leaks and to only do dns lookups once
+const httpsAgent = new https_1.Agent({
+ keepAlive: true
+});
+/* Lower level code related to the Hypixel api */
+const apiKeys = process.env.keys.split(' ');
+const apiKeyUsage = {};
+const baseHypixelAPI = 'https://api.hypixel.net';
+/** Choose the best current API key */
+function chooseApiKey() {
+ // find the api key with the lowest amount of uses
+ let bestKeyUsage = null;
+ let bestKey = null;
+ for (var key of util_1.shuffle(apiKeys)) {
+ 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 || keyUsage.remaining > bestKeyUsage.remaining) {
+ bestKeyUsage = keyUsage;
+ bestKey = key;
+ }
+ }
+ return bestKey;
+}
+exports.chooseApiKey = chooseApiKey;
+/** Send an HTTP request to the Hypixel API */
+async function sendApiRequest({ path, key, args }) {
+ console.log('sending api request to', path, 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 + '?' + util_1.jsonToQuery(args);
+ const fetchResponse = await node_fetch_1.default(fetchUrl, { agent: () => httpsAgent });
+ if (fetchResponse.headers['ratelimit-limit'])
+ // remember how many uses it has
+ apiKeyUsage[key] = {
+ remaining: fetchResponse.headers['ratelimit-remaining'],
+ limit: fetchResponse.headers['ratelimit-limit'],
+ reset: Date.now() + parseInt(fetchResponse.headers['ratelimit-reset']) * 1000
+ };
+ const fetchJsonParsed = await fetchResponse.json();
+ if (fetchJsonParsed.throttle) {
+ apiKeyUsage[key].remaining = 0;
+ console.log('throttled :(');
+ return { throttled: true };
+ }
+ return fetchJsonParsed;
+}
+exports.sendApiRequest = sendApiRequest;
diff --git a/build/hypixelCached.js b/build/hypixelCached.js
new file mode 100644
index 0000000..c874089
--- /dev/null
+++ b/build/hypixelCached.js
@@ -0,0 +1,227 @@
+"use strict";
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+ __setModuleDefault(result, mod);
+ return result;
+};
+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.fetchPlayer = exports.usernameFromUser = exports.uuidFromUser = void 0;
+const node_cache_1 = __importDefault(require("node-cache"));
+const mojang = __importStar(require("./mojang"));
+const hypixel = __importStar(require("./hypixel"));
+const util_1 = require("./util");
+/**
+Hypixel... but with caching
+All the caching in this project is done here!
+*/
+// cache usernames for 4 hours
+const usernameCache = new node_cache_1.default({
+ stdTTL: 60 * 60 * 4,
+ checkperiod: 60,
+ useClones: false,
+});
+const basicProfilesCache = new node_cache_1.default({
+ stdTTL: 60 * 10,
+ checkperiod: 60,
+ useClones: false,
+});
+const playerCache = new node_cache_1.default({
+ stdTTL: 60,
+ checkperiod: 10,
+ useClones: false,
+});
+const profileCache = new node_cache_1.default({
+ stdTTL: 30,
+ checkperiod: 10,
+ useClones: false,
+});
+const profilesCache = new node_cache_1.default({
+ stdTTL: 60 * 3,
+ checkperiod: 10,
+ useClones: false,
+});
+const profileNameCache = new node_cache_1.default({
+ stdTTL: 60 * 60,
+ checkperiod: 60,
+ useClones: false,
+});
+/**
+ * Fetch the uuid from a user
+ * @param user A user can be either a uuid or a username
+ */
+async function uuidFromUser(user) {
+ if (usernameCache.has(util_1.undashUuid(user)))
+ // check if the uuid is a key
+ return util_1.undashUuid(user);
+ // check if the username is a value
+ const uuidToUsername = usernameCache.mget(usernameCache.keys());
+ for (const [uuid, username] of Object.entries(uuidToUsername)) {
+ if (user.toLowerCase() === username.toLowerCase())
+ return uuid;
+ }
+ // not cached, actually fetch mojang api now
+ let { uuid, username } = await mojang.mojangDataFromUser(user);
+ // remove dashes from the uuid so its more normal
+ uuid = util_1.undashUuid(uuid);
+ usernameCache.set(uuid, username);
+ return uuid;
+}
+exports.uuidFromUser = uuidFromUser;
+/**
+ * Fetch the username from a user
+ * @param user A user can be either a uuid or a username
+ */
+async function usernameFromUser(user) {
+ if (usernameCache.has(util_1.undashUuid(user))) {
+ return usernameCache.get(util_1.undashUuid(user));
+ }
+ let { uuid, username } = await mojang.mojangDataFromUser(user);
+ uuid = util_1.undashUuid(uuid);
+ usernameCache.set(uuid, username);
+ return username;
+}
+exports.usernameFromUser = usernameFromUser;
+async function fetchPlayer(user) {
+ const playerUuid = await uuidFromUser(user);
+ if (playerCache.has(playerUuid)) {
+ console.log('cache hit! fetchPlayer', playerUuid);
+ return playerCache.get(playerUuid);
+ }
+ const cleanPlayer = await hypixel.sendCleanApiRequest({
+ path: 'player',
+ args: { uuid: playerUuid }
+ });
+ playerCache.set(playerUuid, cleanPlayer);
+ return cleanPlayer;
+}
+exports.fetchPlayer = fetchPlayer;
+async function fetchSkyblockProfiles(playerUuid) {
+ if (profilesCache.has(playerUuid)) {
+ console.log('cache hit! fetchSkyblockProfiles', playerUuid);
+ return profilesCache.get(playerUuid);
+ }
+ const profiles = await hypixel.sendCleanApiRequest({
+ path: 'skyblock/profiles',
+ args: {
+ uuid: 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
+ };
+ })
+ };
+ basicProfiles.push(basicProfile);
+ }
+ // cache the profiles
+ profilesCache.set(playerUuid, basicProfiles);
+ return basicProfiles;
+}
+exports.fetchSkyblockProfiles = fetchSkyblockProfiles;
+/** Fetch an array of `BasicProfile`s */
+async function fetchBasicProfiles(user) {
+ const playerUuid = await uuidFromUser(user);
+ if (basicProfilesCache.has(playerUuid)) {
+ console.log('cache hit! fetchBasicProfiles');
+ return basicProfilesCache.get(playerUuid);
+ }
+ const player = await fetchPlayer(playerUuid);
+ const profiles = player.profiles;
+ basicProfilesCache.set(playerUuid, profiles);
+ console.log(player);
+ // 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
+ */
+async function fetchProfileUuid(user, profile) {
+ const profiles = await fetchBasicProfiles(user);
+ const profileUuid = util_1.undashUuid(profile);
+ for (const p of profiles) {
+ if (p.name.toLowerCase() === profileUuid.toLowerCase())
+ return util_1.undashUuid(p.uuid);
+ else if (util_1.undashUuid(p.uuid) === util_1.undashUuid(profileUuid))
+ return util_1.undashUuid(p.uuid);
+ }
+}
+exports.fetchProfileUuid = fetchProfileUuid;
+/**
+ * Fetch an entire profile from the user and profile data
+ * @param user A username or uuid
+ * @param profile A profile name or profile uuid
+ */
+async function fetchProfile(user, profile) {
+ const profileUuid = await fetchProfileUuid(user, profile);
+ if (profileCache.has(profileUuid)) {
+ console.log('cache hit! fetchProfile');
+ // we have the profile cached, return it :)
+ return profileCache.get(profileUuid);
+ }
+ const profileName = await fetchProfileName(user, profile);
+ const cleanProfile = await hypixel.sendCleanApiRequest({
+ path: 'skyblock/profile',
+ args: {
+ profile: profileUuid
+ }
+ });
+ // we know the name from fetchProfileName, so set it here
+ cleanProfile.name = profileName;
+ profileCache.set(profileUuid, cleanProfile);
+ return cleanProfile;
+}
+exports.fetchProfile = fetchProfile;
+/**
+ * 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
+ */
+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);
+ const playerUuid = await uuidFromUser(user);
+ if (profileNameCache.has(`${playerUuid}.${profileUuid}`)) {
+ // Return the profile name if it's cached
+ console.log('cache hit! fetchProfileName');
+ return profileNameCache.get(`${playerUuid}.${profileUuid}`);
+ }
+ const basicProfiles = await fetchBasicProfiles(playerUuid);
+ let profileName;
+ for (const basicProfile of basicProfiles)
+ if (basicProfile.uuid === playerUuid)
+ profileName = basicProfile.name;
+ profileNameCache.set(`${playerUuid}.${profileUuid}`, profileName);
+ return profileName;
+}
+exports.fetchProfileName = fetchProfileName;
diff --git a/build/index.js b/build/index.js
new file mode 100644
index 0000000..bfc8b8d
--- /dev/null
+++ b/build/index.js
@@ -0,0 +1,18 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+const express_1 = __importDefault(require("express"));
+const hypixel_1 = require("./hypixel");
+const app = express_1.default();
+app.get('/', async (req, res) => {
+ res.json({ ok: true });
+});
+app.get('/player/:user', async (req, res) => {
+ res.json(await hypixel_1.fetchUser({ user: req.params.user }, ['profiles', 'player']));
+});
+app.get('/player/:user/:profile', async (req, res) => {
+ res.json(await hypixel_1.fetchMemberProfile(req.params.user, req.params.profile));
+});
+app.listen(8080, () => console.log('App started :)'));
diff --git a/build/mojang.js b/build/mojang.js
new file mode 100644
index 0000000..981bf31
--- /dev/null
+++ b/build/mojang.js
@@ -0,0 +1,40 @@
+"use strict";
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.usernameFromUser = exports.uuidFromUser = exports.mojangDataFromUser = void 0;
+const node_fetch_1 = __importDefault(require("node-fetch"));
+const https_1 = require("https");
+// We need to create an agent to prevent memory leaks
+const httpsAgent = new https_1.Agent({
+ keepAlive: true
+});
+/**
+ * Get mojang api data from ashcon.app
+ */
+async function mojangDataFromUser(user) {
+ console.log('cache miss :( mojangDataFromUser', user);
+ const fetchResponse = await node_fetch_1.default('https://api.ashcon.app/mojang/v2/user/' + user, { agent: () => httpsAgent });
+ return await fetchResponse.json();
+}
+exports.mojangDataFromUser = mojangDataFromUser;
+/**
+ * Fetch the uuid from a user
+ * @param user A user can be either a uuid or a username
+ */
+async function uuidFromUser(user) {
+ const fetchJSON = await mojangDataFromUser(user);
+ return fetchJSON.uuid.replace(/-/g, '');
+}
+exports.uuidFromUser = uuidFromUser;
+/**
+ * Fetch the username from a user
+ * @param user A user can be either a uuid or a username
+ */
+async function usernameFromUser(user) {
+ // get a minecraft uuid from a username, using ashcon.app's mojang api
+ const fetchJSON = await mojangDataFromUser(user);
+ return fetchJSON.username;
+}
+exports.usernameFromUser = usernameFromUser;
diff --git a/build/util.js b/build/util.js
new file mode 100644
index 0000000..4c7fe64
--- /dev/null
+++ b/build/util.js
@@ -0,0 +1,78 @@
+"use strict";
+/* Utility functions (not related to Hypixel) */
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.colorCodeFromName = exports.minecraftColorCodes = exports.shuffle = exports.jsonToQuery = exports.queryToJson = exports.undashUuid = void 0;
+function undashUuid(uuid) {
+ return uuid.replace(/-/g, '');
+}
+exports.undashUuid = undashUuid;
+function queryToJson(queryString) {
+ var query = {};
+ var pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
+ for (var i = 0; i < pairs.length; i++) {
+ var pair = pairs[i].split('=');
+ query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
+ }
+ return query;
+}
+exports.queryToJson = queryToJson;
+function jsonToQuery(data) {
+ return Object.entries(data || {}).map(e => e.join('=')).join('&');
+}
+exports.jsonToQuery = jsonToQuery;
+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;
+}
+exports.shuffle = shuffle;
+exports.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)
+ */
+function colorCodeFromName(colorName) {
+ const hexColor = exports.minecraftColorCodes[colorName.toLowerCase()];
+ for (const key in exports.minecraftColorCodes) {
+ const value = exports.minecraftColorCodes[key];
+ if (key.length === 1 && value === hexColor)
+ return key;
+ }
+}
+exports.colorCodeFromName = colorCodeFromName;
diff --git a/package.json b/package.json
index 4b544ef..c08b643 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"main": "index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
- "compile": ""
+ "compile": "tsc"
},
"repository": {
"type": "git",