aboutsummaryrefslogtreecommitdiff
path: root/build
diff options
context:
space:
mode:
Diffstat (limited to 'build')
-rw-r--r--build/cleaners/skyblock/member.js3
-rw-r--r--build/cleaners/skyblock/profile.js4
-rw-r--r--build/cleaners/skyblock/profiles.js3
-rw-r--r--build/constants.js111
-rw-r--r--build/database.js115
-rw-r--r--build/hypixel.js35
-rw-r--r--build/hypixelCached.js62
-rw-r--r--build/index.js2
-rw-r--r--build/util.js2
9 files changed, 315 insertions, 22 deletions
diff --git a/build/cleaners/skyblock/member.js b/build/cleaners/skyblock/member.js
index 8b922a5..2e07f39 100644
--- a/build/cleaners/skyblock/member.js
+++ b/build/cleaners/skyblock/member.js
@@ -43,6 +43,7 @@ async function cleanSkyBlockProfileMemberResponseBasic(member, included = null)
exports.cleanSkyBlockProfileMemberResponseBasic = cleanSkyBlockProfileMemberResponseBasic;
/** Cleans up a member (from skyblock/profile) */
async function cleanSkyBlockProfileMemberResponse(member, included = null) {
+ var _a;
// profiles.members[]
const inventoriesIncluded = included == null || included.includes('inventories');
const player = await cached.fetchPlayer(member.uuid);
@@ -54,6 +55,8 @@ async function cleanSkyBlockProfileMemberResponse(member, included = null) {
rank: player.rank,
purse: member.coin_purse,
stats: stats_1.cleanProfileStats(member),
+ // this is used for leaderboards
+ rawHypixelStats: (_a = member.stats) !== null && _a !== void 0 ? _a : {},
minions: minions_1.cleanMinions(member),
fairy_souls: fairysouls_1.cleanFairySouls(member),
inventories: inventoriesIncluded ? await inventory_1.cleanInventories(member) : undefined,
diff --git a/build/cleaners/skyblock/profile.js b/build/cleaners/skyblock/profile.js
index 2b18ff8..fe1161e 100644
--- a/build/cleaners/skyblock/profile.js
+++ b/build/cleaners/skyblock/profile.js
@@ -25,12 +25,12 @@ exports.cleanSkyblockProfileResponseLighter = cleanSkyblockProfileResponseLighte
/**
* This function is somewhat costly and shouldn't be called often. Use cleanSkyblockProfileResponseLighter if you don't need all the data
*/
-async function cleanSkyblockProfileResponse(data, { mainMemberUuid }) {
+async function cleanSkyblockProfileResponse(data, options) {
const cleanedMembers = [];
for (const memberUUID in data.members) {
const memberRaw = data.members[memberUUID];
memberRaw.uuid = memberUUID;
- const member = await member_1.cleanSkyBlockProfileMemberResponse(memberRaw, ['stats', mainMemberUuid === memberUUID ? 'inventories' : undefined]);
+ const member = await member_1.cleanSkyBlockProfileMemberResponse(memberRaw, ['stats', (options === null || options === void 0 ? void 0 : options.mainMemberUuid) === memberUUID ? 'inventories' : undefined]);
cleanedMembers.push(member);
}
const memberMinions = [];
diff --git a/build/cleaners/skyblock/profiles.js b/build/cleaners/skyblock/profiles.js
index b4eb07b..029110a 100644
--- a/build/cleaners/skyblock/profiles.js
+++ b/build/cleaners/skyblock/profiles.js
@@ -17,7 +17,8 @@ exports.cleanPlayerSkyblockProfiles = cleanPlayerSkyblockProfiles;
async function cleanSkyblockProfilesResponse(data) {
const cleanedProfiles = [];
for (const profile of data !== null && data !== void 0 ? data : []) {
- let cleanedProfile = await profile_1.cleanSkyblockProfileResponseLighter(profile);
+ // let cleanedProfile = await cleanSkyblockProfileResponseLighter(profile)
+ let cleanedProfile = await profile_1.cleanSkyblockProfileResponse(profile);
cleanedProfiles.push(cleanedProfile);
}
return cleanedProfiles;
diff --git a/build/constants.js b/build/constants.js
new file mode 100644
index 0000000..23d2b75
--- /dev/null
+++ b/build/constants.js
@@ -0,0 +1,111 @@
+"use strict";
+/**
+ * Fetch and edit constants from the skyblock-constants repo
+ */
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.addStats = exports.fetchStats = void 0;
+const node_fetch_1 = __importDefault(require("node-fetch"));
+const https_1 = require("https");
+const node_cache_1 = __importDefault(require("node-cache"));
+const httpsAgent = new https_1.Agent({
+ keepAlive: true
+});
+const githubApiBase = 'https://api.github.com';
+const owner = 'skyblockstats';
+const repo = 'skyblock-constants';
+/**
+ * Send a request to the GitHub API
+ * @param method The HTTP method, for example GET, PUT, POST, etc
+ * @param route The route to send the request to
+ * @param headers The extra headers
+ * @param json The JSON body, only applicable for some types of methods
+ */
+async function fetchGithubApi(method, route, headers, json) {
+ 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),
+ });
+}
+// cache files for a day
+const fileCache = new node_cache_1.default({
+ stdTTL: 60 * 60 * 24,
+ checkperiod: 60,
+ useClones: false,
+});
+/**
+ * Fetch a file from skyblock-constants
+ * @param path The file path, for example stats.json
+ */
+async function fetchFile(path) {
+ if (fileCache.has(path))
+ return fileCache.get(path);
+ const r = await fetchGithubApi('GET', `/repos/${owner}/${repo}/contents/${path}`, { 'Accept': 'application/vnd.github.v3+json' });
+ const data = await r.json();
+ return {
+ path: data.path,
+ content: Buffer.from(data.content, data.encoding).toString(),
+ sha: data.sha
+ };
+}
+/**
+ * Edit a file on skyblock-constants
+ * @param file The GithubFile you got from fetchFile
+ * @param message The commit message
+ * @param newContent The new content in the file
+ */
+async function editFile(file, message, newContent) {
+ fileCache.set(file.path, newContent);
+ await fetchGithubApi('PUT', `/repos/${owner}/${repo}/contents/${file.path}`, { 'Content-Type': 'application/json' }, {
+ message: message,
+ content: Buffer.from(newContent).toString('base64'),
+ sha: file.sha,
+ branch: 'main'
+ });
+}
+/** Fetch all the known SkyBlock stats as an array of strings */
+async function fetchStats() {
+ const file = await fetchFile('stats.json');
+ try {
+ return JSON.parse(file.content);
+ }
+ catch {
+ // probably invalid json, return an empty array
+ return [];
+ }
+}
+exports.fetchStats = fetchStats;
+/** Add stats to skyblock-constants. This has caching so it's fine to call many times */
+async function addStats(addingStats) {
+ if (addingStats.length === 0)
+ return; // no stats provided, just return
+ const file = await fetchFile('stats.json');
+ if (!file.path)
+ return;
+ let oldStats;
+ try {
+ oldStats = JSON.parse(file.content);
+ }
+ catch {
+ // invalid json, set it as an empty array
+ oldStats = [];
+ }
+ const updatedStats = oldStats
+ .concat(addingStats)
+ // remove duplicates
+ .filter((value, index, array) => array.indexOf(value) === index)
+ .sort((a, b) => a.localeCompare(b));
+ const newStats = updatedStats.filter(value => !oldStats.includes(value));
+ // there's not actually any new stats, just return
+ if (newStats.length === 0)
+ return;
+ const commitMessage = newStats.length >= 2 ? `Add ${newStats.length} new stats` : `Add '${newStats[0]}'`;
+ await editFile(file, commitMessage, JSON.stringify(updatedStats, null, 2));
+}
+exports.addStats = addStats;
diff --git a/build/database.js b/build/database.js
new file mode 100644
index 0000000..31c85ee
--- /dev/null
+++ b/build/database.js
@@ -0,0 +1,115 @@
+"use strict";
+/**
+ * Store data about members for leaderboards
+*/
+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.updateDatabaseMember = void 0;
+const constants = __importStar(require("./constants"));
+const mongodb_1 = require("mongodb");
+const node_cache_1 = __importDefault(require("node-cache"));
+// don't update the user for 3 minutes
+const recentlyUpdated = new node_cache_1.default({
+ stdTTL: 60 * 3,
+ checkperiod: 60,
+ useClones: false,
+});
+const cachedLeaderboards = new Map();
+let client;
+let database;
+let memberLeaderboardsCollection;
+async function connect() {
+ if (!process.env.db_uri)
+ return console.warn('Warning: db_uri was not found in .env. Features that utilize the database such as leaderboards won\'t work.');
+ if (!process.env.db_name)
+ return console.warn('Warning: db_name was not found in .env. Features that utilize the database such as leaderboards won\'t work.');
+ client = await mongodb_1.MongoClient.connect(process.env.db_uri, { useNewUrlParser: true, useUnifiedTopology: true });
+ database = client.db(process.env.db_name);
+ memberLeaderboardsCollection = database.collection('member-leaderboards');
+}
+function getMemberCollectionAttributes(member) {
+ const collectionAttributes = {};
+ for (const collection of member.collections) {
+ const collectionLeaderboardName = `collection_${collection.name}`;
+ collectionAttributes[collectionLeaderboardName] = collection.xp;
+ }
+ return collectionAttributes;
+}
+function getMemberLeaderboardAttributes(member) {
+ // if you want to add a new leaderboard for member attributes, add it here
+ return {
+ // we use the raw stat names rather than the clean stats in case hypixel adds a new stat and it takes a while for us to clean it
+ ...member.rawHypixelStats,
+ // collection leaderboards
+ ...getMemberCollectionAttributes(member),
+ fairy_souls: member.fairy_souls.total,
+ first_join: member.first_join,
+ purse: member.purse,
+ visited_zones: member.visited_zones.length,
+ };
+}
+async function fetchMemberLeaderboard(name) {
+ if (cachedLeaderboards.has(name))
+ return cachedLeaderboards.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 };
+ const sortQuery = {};
+ sortQuery[`stats.${name}`] = 1;
+ const leaderboard = await memberLeaderboardsCollection.find(query).sort(sortQuery).toArray();
+ cachedLeaderboards.set(name, leaderboard);
+ return leaderboard;
+}
+async function getLeaderboardRequirement(name) {
+ const leaderboard = await fetchMemberLeaderboard(name);
+ // if there's more than 100 items, return the 100th. if there's less, return null
+ if (leaderboard.length > 100)
+ return leaderboard[100].stats[name];
+ else
+ return null;
+}
+/** Update the member's leaderboard data on the server if applicable */
+async function updateDatabaseMember(member) {
+ if (!client)
+ return; // the db client hasn't been initialized
+ // the member's been updated too recently, just return
+ if (recentlyUpdated.get(member.uuid))
+ return;
+ // store the member in recentlyUpdated so it cant update for 3 more minutes
+ recentlyUpdated.set(member.uuid, true);
+ await constants.addStats(Object.keys(member.rawHypixelStats));
+ const leaderboardAttributes = getMemberLeaderboardAttributes(member);
+ await memberLeaderboardsCollection.updateOne({
+ uuid: member.uuid
+ }, {
+ '$set': {
+ 'stats': leaderboardAttributes,
+ 'last_updated': new Date()
+ }
+ }, {
+ upsert: true
+ });
+}
+exports.updateDatabaseMember = updateDatabaseMember;
+connect();
diff --git a/build/hypixel.js b/build/hypixel.js
index 6e6b8b6..97e24b0 100644
--- a/build/hypixel.js
+++ b/build/hypixel.js
@@ -22,17 +22,19 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
-exports.fetchMemberProfile = exports.fetchUser = exports.sendCleanApiRequest = exports.maxMinion = exports.saveInterval = void 0;
+exports.fetchMemberProfilesUncached = 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"));
const profile_1 = require("./cleaners/skyblock/profile");
const profiles_1 = require("./cleaners/skyblock/profiles");
const _1 = require(".");
+const database_1 = require("./database");
// 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;
+/** Sends an API request to Hypixel and cleans it up. */
async function sendCleanApiRequest({ path, args }, included, options) {
const key = await hypixelApi_1.chooseApiKey();
const rawResponse = await hypixelApi_1.sendApiRequest({ path, key, args });
@@ -143,3 +145,34 @@ async function fetchMemberProfile(user, profile) {
};
}
exports.fetchMemberProfile = fetchMemberProfile;
+/**
+ * Fetches the Hypixel API to get a CleanFullProfile. This doesn't do any caching and you should use hypixelCached.fetchProfile instead
+ * @param playerUuid The UUID of the Minecraft player
+ * @param profileUuid The UUID of the Hypixel SkyBlock profile
+ */
+async function fetchMemberProfileUncached(playerUuid, profileUuid) {
+ const profile = await sendCleanApiRequest({
+ path: 'skyblock/profile',
+ args: { profile: profileUuid }
+ }, null, { mainMemberUuid: playerUuid });
+ for (const member of profile.members)
+ database_1.updateDatabaseMember(member);
+ return profile;
+}
+exports.fetchMemberProfileUncached = fetchMemberProfileUncached;
+async function fetchMemberProfilesUncached(playerUuid) {
+ const profiles = await sendCleanApiRequest({
+ path: 'skyblock/profiles',
+ args: {
+ uuid: playerUuid
+ }
+ }, null, {
+ // only the inventories for the main player are generated, this is for optimization purposes
+ mainMemberUuid: playerUuid
+ });
+ for (const profile of profiles)
+ for (const member of profile.members)
+ database_1.updateDatabaseMember(member);
+ return profiles;
+}
+exports.fetchMemberProfilesUncached = fetchMemberProfilesUncached;
diff --git a/build/hypixelCached.js b/build/hypixelCached.js
index 2769a42..5a970f8 100644
--- a/build/hypixelCached.js
+++ b/build/hypixelCached.js
@@ -62,26 +62,53 @@ const profileNameCache = new node_cache_1.default({
checkperiod: 60,
useClones: false,
});
+function waitForSet(cache, key, value) {
+ return new Promise((resolve, reject) => {
+ const listener = (setKey, setValue) => {
+ if (setKey === key || (value && setValue === value)) {
+ cache.removeListener('set', listener);
+ return resolve({ key, value });
+ }
+ };
+ cache.on('set', listener);
+ });
+}
/**
* 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)))
+ if (usernameCache.has(util_1.undashUuid(user))) {
// check if the uuid is a key
- return util_1.undashUuid(user);
+ const username = usernameCache.get(util_1.undashUuid(user));
+ // if it has .then, then that means its a waitForSet promise
+ if (username.then) {
+ console.log('pog, prevented double request');
+ return (await username()).key;
+ }
+ else
+ 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;
}
+ if (_1.debug)
+ console.log('Cache miss: uuidFromUser', user);
+ // set it as waitForSet (a promise) in case uuidFromUser gets called while its fetching mojang
+ console.log('setting', util_1.undashUuid(user));
+ usernameCache.set(util_1.undashUuid(user), waitForSet(usernameCache, user, user));
+ console.log(util_1.undashUuid(user), usernameCache.has(util_1.undashUuid(user)));
// not cached, actually fetch mojang api now
let { uuid, username } = await mojang.mojangDataFromUser(user);
if (!uuid)
return;
// remove dashes from the uuid so its more normal
uuid = util_1.undashUuid(uuid);
+ if (user !== uuid)
+ usernameCache.del(user);
usernameCache.set(uuid, username);
return uuid;
}
@@ -96,6 +123,8 @@ async function usernameFromUser(user) {
console.log('Cache hit! usernameFromUser', user);
return usernameCache.get(util_1.undashUuid(user));
}
+ if (_1.debug)
+ console.log('Cache miss: usernameFromUser', user);
let { uuid, username } = await mojang.mojangDataFromUser(user);
uuid = util_1.undashUuid(uuid);
usernameCache.set(uuid, username);
@@ -106,9 +135,11 @@ async function fetchPlayer(user) {
const playerUuid = await uuidFromUser(user);
if (playerCache.has(playerUuid)) {
if (_1.debug)
- console.log('Cache hit! fetchPlayer', playerUuid);
+ console.log('Cache hit! fetchPlayer', user);
return playerCache.get(playerUuid);
}
+ if (_1.debug)
+ console.log('Cache miss: uuidFromUser', user);
const cleanPlayer = await hypixel.sendCleanApiRequest({
path: 'player',
args: { uuid: playerUuid }
@@ -126,15 +157,9 @@ async function fetchSkyblockProfiles(playerUuid) {
console.log('Cache hit! fetchSkyblockProfiles', playerUuid);
return profilesCache.get(playerUuid);
}
- const profiles = await hypixel.sendCleanApiRequest({
- path: 'skyblock/profiles',
- args: {
- uuid: playerUuid
- }
- }, null, {
- // only the inventories for the main player are generated, this is for optimization purposes
- mainMemberUuid: playerUuid
- });
+ if (_1.debug)
+ console.log('Cache miss: fetchSkyblockProfiles', playerUuid);
+ const profiles = await hypixel.fetchMemberProfilesUncached(playerUuid);
const basicProfiles = [];
// create the basicProfiles array
for (const profile of profiles) {
@@ -166,6 +191,8 @@ async function fetchBasicProfiles(user) {
console.log('Cache hit! fetchBasicProfiles', playerUuid);
return basicProfilesCache.get(playerUuid);
}
+ if (_1.debug)
+ console.log('Cache miss: fetchBasicProfiles', user);
const player = await fetchPlayer(playerUuid);
const profiles = player.profiles;
basicProfilesCache.set(playerUuid, profiles);
@@ -186,6 +213,8 @@ async function fetchProfileUuid(user, profile) {
console.log('no profile provided?', user, profile);
return null;
}
+ if (_1.debug)
+ console.log('Cache miss: fetchProfileUuid', user);
const profiles = await fetchBasicProfiles(user);
const profileUuid = util_1.undashUuid(profile);
for (const p of profiles) {
@@ -210,11 +239,10 @@ async function fetchProfile(user, profile) {
console.log('Cache hit! fetchProfile', profileUuid);
return profileCache.get(profileUuid);
}
+ if (_1.debug)
+ console.log('Cache miss: fetchProfile', user, profile);
const profileName = await fetchProfileName(user, profile);
- const cleanProfile = await hypixel.sendCleanApiRequest({
- path: 'skyblock/profile',
- args: { profile: profileUuid }
- }, null, { mainMemberUuid: playerUuid });
+ const cleanProfile = await hypixel.fetchMemberProfileUncached(playerUuid, profileUuid);
// we know the name from fetchProfileName, so set it here
cleanProfile.name = profileName;
profileCache.set(profileUuid, cleanProfile);
@@ -236,6 +264,8 @@ async function fetchProfileName(user, profile) {
console.log('Cache hit! fetchProfileName', profileUuid);
return profileNameCache.get(`${playerUuid}.${profileUuid}`);
}
+ if (_1.debug)
+ console.log('Cache miss: fetchProfileName', user, profile);
const basicProfiles = await fetchBasicProfiles(playerUuid);
let profileName;
for (const basicProfile of basicProfiles)
diff --git a/build/index.js b/build/index.js
index b089351..3174e69 100644
--- a/build/index.js
+++ b/build/index.js
@@ -7,7 +7,7 @@ exports.debug = void 0;
const hypixel_1 = require("./hypixel");
const express_1 = __importDefault(require("express"));
const app = express_1.default();
-exports.debug = false;
+exports.debug = true;
app.use((req, res, next) => {
if (process.env.key && req.headers.key !== process.env.key)
// if a key is set in process.env and the header doesn't match return an error
diff --git a/build/util.js b/build/util.js
index 3350acf..3b21db4 100644
--- a/build/util.js
+++ b/build/util.js
@@ -5,7 +5,7 @@
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, '');
+ return uuid.replace(/-/g, '').toLowerCase();
}
exports.undashUuid = undashUuid;
function queryToJson(queryString) {