aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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
-rw-r--r--package-lock.json1051
-rw-r--r--package.json3
-rw-r--r--src/cleaners/rank.ts84
-rw-r--r--src/cleaners/skyblock/member.ts9
-rw-r--r--src/cleaners/skyblock/profile.ts25
-rw-r--r--src/cleaners/skyblock/profiles.ts5
-rw-r--r--src/constants.ts172
-rw-r--r--src/database.ts173
-rw-r--r--src/hypixel.ts230
-rw-r--r--src/hypixelCached.ts71
-rw-r--r--src/index.ts8
-rw-r--r--src/util.ts2
21 files changed, 1972 insertions, 198 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) {
diff --git a/package-lock.json b/package-lock.json
index 9fa143f..b2561d7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,918 @@
{
"name": "skyblock-api",
"version": "1.0.0",
- "lockfileVersion": 1,
+ "lockfileVersion": 2,
"requires": true,
+ "packages": {
+ "": {
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "express": "^4.17.1",
+ "mongodb": "^3.6.4",
+ "node-cache": "^5.1.2",
+ "node-fetch": "^2.6.1",
+ "prismarine-nbt": "^1.4.0"
+ },
+ "devDependencies": {
+ "@types/mongodb": "^3.6.8",
+ "@types/node": "^14.14.28",
+ "@types/node-fetch": "^2.5.8",
+ "dotenv": "^8.2.0"
+ }
+ },
+ "node_modules/@types/bson": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.3.tgz",
+ "integrity": "sha512-mVRvYnTOZJz3ccpxhr3wgxVmSeiYinW+zlzQz3SXWaJmD1DuL05Jeq7nKw3SnbKmbleW5qrLG5vdyWe/A9sXhw==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/mongodb": {
+ "version": "3.6.8",
+ "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.8.tgz",
+ "integrity": "sha512-8qNbL5/GFrljXc/QijcuQcUMYZ1iWNcqnJ6tneROwbfU0LsAjQ9bmq3aHi5lWXM4cyBPd2F/n9INAk/pZZttHw==",
+ "dev": true,
+ "dependencies": {
+ "@types/bson": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "14.14.28",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.28.tgz",
+ "integrity": "sha512-lg55ArB+ZiHHbBBttLpzD07akz0QPrZgUODNakeC09i62dnrywr9mFErHuaPlB6I7z+sEbK+IYmplahvplCj2g==",
+ "dev": true
+ },
+ "node_modules/@types/node-fetch": {
+ "version": "2.5.8",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.8.tgz",
+ "integrity": "sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^3.0.0"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+ "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+ "dependencies": {
+ "mime-types": "~2.1.24",
+ "negotiator": "0.6.2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+ "dev": true
+ },
+ "node_modules/bl": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
+ "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
+ "dependencies": {
+ "readable-stream": "^2.3.5",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "node_modules/bl/node_modules/readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/bl/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+ "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
+ "dependencies": {
+ "bytes": "3.1.0",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "on-finished": "~2.3.0",
+ "qs": "6.7.0",
+ "raw-body": "2.4.0",
+ "type-is": "~1.6.17"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/bson": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz",
+ "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==",
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
+ "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+ "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/denque": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz",
+ "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+ },
+ "node_modules/dotenv": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
+ "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.17.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
+ "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
+ "dependencies": {
+ "accepts": "~1.3.7",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.19.0",
+ "content-disposition": "0.5.3",
+ "content-type": "~1.0.4",
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.1.2",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.5",
+ "qs": "6.7.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.1.2",
+ "send": "0.17.1",
+ "serve-static": "1.14.1",
+ "setprototypeof": "1.1.1",
+ "statuses": "~1.5.0",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+ },
+ "node_modules/finalhandler": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+ "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
+ "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
+ "dev": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+ "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
+ "dependencies": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.1",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+ },
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
+ },
+ "node_modules/lodash.reduce": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
+ "integrity": "sha1-8atrg5KZrUj3hKu/R2WW8DuRTTs="
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/memory-pager": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+ "optional": true
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.45.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz",
+ "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.28",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz",
+ "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==",
+ "dependencies": {
+ "mime-db": "1.45.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mongodb": {
+ "version": "3.6.4",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.4.tgz",
+ "integrity": "sha512-Y+Ki9iXE9jI+n9bVtbTOOdK0B95d6wVGSucwtBkvQ+HIvVdTCfpVRp01FDC24uhC/Q2WXQ8Lpq3/zwtB5Op9Qw==",
+ "dependencies": {
+ "bl": "^2.2.1",
+ "bson": "^1.1.4",
+ "denque": "^1.4.1",
+ "require_optional": "^1.0.1",
+ "safe-buffer": "^5.1.2"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "optionalDependencies": {
+ "saslprep": "^1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "aws4": {
+ "optional": true
+ },
+ "bson-ext": {
+ "optional": true
+ },
+ "kerberos": {
+ "optional": true
+ },
+ "mongodb-client-encryption": {
+ "optional": true
+ },
+ "mongodb-extjson": {
+ "optional": true
+ },
+ "snappy": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-cache": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz",
+ "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==",
+ "dependencies": {
+ "clone": "2.x"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz",
+ "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==",
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "node_modules/prismarine-nbt": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/prismarine-nbt/-/prismarine-nbt-1.4.0.tgz",
+ "integrity": "sha512-CBv6I7rXN6E55AjCg5emA78kgssqWvZeTj1NdG24ZjhZ0YsAKIaopMLek81H8uv/rSz6BbhOSjuSMpHSv9ipyQ==",
+ "dependencies": {
+ "protodef": "^1.7.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+ },
+ "node_modules/protodef": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/protodef/-/protodef-1.9.0.tgz",
+ "integrity": "sha512-RZZtHOT8Q8AaKEbKsPlndivoPOOjm+ddHFVwooBMNTw8XMN4cRo4tKTNCyXf12+IIkzkPDgbAwKjLHCJXYI3HQ==",
+ "dependencies": {
+ "lodash.get": "^4.4.2",
+ "lodash.reduce": "^4.6.0",
+ "protodef-validator": "^1.2.2",
+ "readable-stream": "^3.0.3"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/protodef-validator": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/protodef-validator/-/protodef-validator-1.2.3.tgz",
+ "integrity": "sha512-dMcSMYRh8s0z0iQN0PLVlXwJOgN8cgBuM1uWzhMjkLdpKCOASwp+h7wHnTigBTRVhGLywykcb3EKiGSsXX4vvA==",
+ "dependencies": {
+ "ajv": "^6.5.4"
+ },
+ "bin": {
+ "protodef-validator": "cli.js"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
+ "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
+ "dependencies": {
+ "forwarded": "~0.1.2",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+ "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
+ "dependencies": {
+ "bytes": "3.1.0",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/require_optional": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
+ "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
+ "dependencies": {
+ "resolve-from": "^2.0.0",
+ "semver": "^5.1.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
+ "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/saslprep": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
+ "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
+ "optional": true,
+ "dependencies": {
+ "sparse-bitfield": "^3.0.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
+ "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "~1.7.2",
+ "mime": "1.6.0",
+ "ms": "2.1.1",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.1",
+ "statuses": "~1.5.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ },
+ "node_modules/serve-static": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
+ "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
+ "dependencies": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.17.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
+ },
+ "node_modules/sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
+ "optional": true,
+ "dependencies": {
+ "memory-pager": "^1.0.2"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string_decoder/node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ }
+ },
"dependencies": {
+ "@types/bson": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.3.tgz",
+ "integrity": "sha512-mVRvYnTOZJz3ccpxhr3wgxVmSeiYinW+zlzQz3SXWaJmD1DuL05Jeq7nKw3SnbKmbleW5qrLG5vdyWe/A9sXhw==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/mongodb": {
+ "version": "3.6.8",
+ "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.8.tgz",
+ "integrity": "sha512-8qNbL5/GFrljXc/QijcuQcUMYZ1iWNcqnJ6tneROwbfU0LsAjQ9bmq3aHi5lWXM4cyBPd2F/n9INAk/pZZttHw==",
+ "dev": true,
+ "requires": {
+ "@types/bson": "*",
+ "@types/node": "*"
+ }
+ },
"@types/node": {
"version": "14.14.28",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.28.tgz",
"integrity": "sha512-lg55ArB+ZiHHbBBttLpzD07akz0QPrZgUODNakeC09i62dnrywr9mFErHuaPlB6I7z+sEbK+IYmplahvplCj2g==",
"dev": true
},
+ "@types/node-fetch": {
+ "version": "2.5.8",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.8.tgz",
+ "integrity": "sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "form-data": "^3.0.0"
+ }
+ },
"accepts": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
@@ -35,6 +938,45 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
+ "dev": true
+ },
+ "bl": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
+ "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
+ "requires": {
+ "readable-stream": "^2.3.5",
+ "safe-buffer": "^5.1.1"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
@@ -52,6 +994,11 @@
"type-is": "~1.6.17"
}
},
+ "bson": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz",
+ "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg=="
+ },
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -62,6 +1009,15 @@
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18="
},
+ "combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
@@ -85,6 +1041,11 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
},
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+ },
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -93,6 +1054,17 @@
"ms": "2.0.0"
}
},
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+ "dev": true
+ },
+ "denque": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz",
+ "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ=="
+ },
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -190,6 +1162,17 @@
"unpipe": "~1.0.0"
}
},
+ "form-data": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
+ "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
+ "dev": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ },
"forwarded": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
@@ -230,6 +1213,11 @@
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
},
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+ },
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -250,6 +1238,12 @@
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
+ "memory-pager": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+ "optional": true
+ },
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@@ -278,6 +1272,19 @@
"mime-db": "1.45.0"
}
},
+ "mongodb": {
+ "version": "3.6.4",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.4.tgz",
+ "integrity": "sha512-Y+Ki9iXE9jI+n9bVtbTOOdK0B95d6wVGSucwtBkvQ+HIvVdTCfpVRp01FDC24uhC/Q2WXQ8Lpq3/zwtB5Op9Qw==",
+ "requires": {
+ "bl": "^2.2.1",
+ "bson": "^1.1.4",
+ "denque": "^1.4.1",
+ "require_optional": "^1.0.1",
+ "safe-buffer": "^5.1.2",
+ "saslprep": "^1.0.0"
+ }
+ },
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -327,6 +1334,11 @@
"protodef": "^1.7.0"
}
},
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
+ },
"protodef": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/protodef/-/protodef-1.9.0.tgz",
@@ -391,6 +1403,20 @@
"util-deprecate": "^1.0.1"
}
},
+ "require_optional": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
+ "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
+ "requires": {
+ "resolve-from": "^2.0.0",
+ "semver": "^5.1.0"
+ }
+ },
+ "resolve-from": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
+ "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
+ },
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -401,6 +1427,20 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
+ "saslprep": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
+ "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
+ "optional": true,
+ "requires": {
+ "sparse-bitfield": "^3.0.3"
+ }
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
+ },
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
@@ -444,6 +1484,15 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
+ "sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
+ "optional": true,
+ "requires": {
+ "memory-pager": "^1.0.2"
+ }
+ },
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
diff --git a/package.json b/package.json
index 1cd9c4e..e04bcfb 100644
--- a/package.json
+++ b/package.json
@@ -26,12 +26,15 @@
"homepage": "https://github.com/mat-1/skyblock-api#readme",
"dependencies": {
"express": "^4.17.1",
+ "mongodb": "^3.6.4",
"node-cache": "^5.1.2",
"node-fetch": "^2.6.1",
"prismarine-nbt": "^1.4.0"
},
"devDependencies": {
+ "@types/mongodb": "^3.6.8",
"@types/node": "^14.14.28",
+ "@types/node-fetch": "^2.5.8",
"dotenv": "^8.2.0"
}
}
diff --git a/src/cleaners/rank.ts b/src/cleaners/rank.ts
index d565502..0a3a4a7 100644
--- a/src/cleaners/rank.ts
+++ b/src/cleaners/rank.ts
@@ -22,48 +22,50 @@ export interface CleanRank {
/** Response cleaning (reformatting to be nicer) */
export function cleanRank({
- packageRank,
- newPackageRank,
- monthlyPackageRank,
- rankPlusColor,
- rank,
- prefix
+ packageRank,
+ newPackageRank,
+ monthlyPackageRank,
+ rankPlusColor,
+ rank,
+ prefix
}: HypixelPlayer): CleanRank {
- let name
- let color
- let colored
- if (prefix) { // derive values from prefix
- colored = prefix
- color = minecraftColorCodes[colored.match(/§./)[0][1]]
- name = colored.replace(/§./g, '').replace(/[\[\]]/g, '')
- } else {
- name = monthlyPackageRank
- || rank
- || newPackageRank?.replace('_PLUS', '+')
- || packageRank?.replace('_PLUS', '+')
+ let name
+ let color
+ let colored
+ if (prefix) { // derive values from prefix
+ colored = prefix
+ color = minecraftColorCodes[colored.match(/§./)[0][1]]
+ name = colored.replace(/§./g, '').replace(/[\[\]]/g, '')
+ } else {
+ if (monthlyPackageRank !== 'NONE')
+ name = monthlyPackageRank
+ else
+ name = rank
+ || newPackageRank?.replace('_PLUS', '+')
+ || packageRank?.replace('_PLUS', '+')
- // 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'
- else if (name === undefined) name = 'NONE'
+ // 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'
+ else if (name === undefined) name = 'NONE'
- const plusColor = rankPlusColor ? colorCodeFromName(rankPlusColor) : null
- color = minecraftColorCodes[rankColors[name]]
- const rankColorPrefix = rankColors[name] ? '§' + rankColors[name] : ''
- const nameWithoutPlus = name.split('+')[0]
- const plusesInName = '+'.repeat(name.split('+').length - 1)
- if (plusColor && plusesInName.length >= 1)
- colored = `${rankColorPrefix}[${nameWithoutPlus}§${plusColor}${plusesInName}${rankColorPrefix}]`
- else if (name !== 'NONE')
- colored = `${rankColorPrefix}[${name}]`
- else
- // nons don't have a prefix
- colored = `${rankColorPrefix}`
- }
- return {
- name,
- color,
- colored
- }
+ const plusColor = rankPlusColor ? colorCodeFromName(rankPlusColor) : null
+ color = minecraftColorCodes[rankColors[name]]
+ const rankColorPrefix = rankColors[name] ? '§' + rankColors[name] : ''
+ const nameWithoutPlus = name.split('+')[0]
+ const plusesInName = '+'.repeat(name.split('+').length - 1)
+ if (plusColor && plusesInName.length >= 1)
+ colored = `${rankColorPrefix}[${nameWithoutPlus}§${plusColor}${plusesInName}${rankColorPrefix}]`
+ else if (name !== 'NONE')
+ colored = `${rankColorPrefix}[${name}]`
+ else
+ // nons don't have a prefix
+ colored = `${rankColorPrefix}`
+ }
+ return {
+ name,
+ color,
+ colored
+ }
}
diff --git a/src/cleaners/skyblock/member.ts b/src/cleaners/skyblock/member.ts
index 154ff22..a4ca053 100644
--- a/src/cleaners/skyblock/member.ts
+++ b/src/cleaners/skyblock/member.ts
@@ -5,7 +5,7 @@ import { cleanObjectives, Objective } from './objectives'
import { CleanMinion, cleanMinions } from './minions'
import { cleanSkills, Skill } from './skills'
import * as cached from '../../hypixelCached'
-import { CleanFullProfile } from './profile'
+import { CleanFullProfile, CleanFullProfileBasicMembers } from './profile'
import { Included } from '../../hypixel'
import { CleanPlayer } from '../player'
import { Bank } from './bank'
@@ -25,6 +25,7 @@ export interface CleanBasicMember {
export interface CleanMember extends CleanBasicMember {
purse: number
stats: CleanProfileStats
+ rawHypixelStats?: { [ key: string ]: number }
minions: CleanMinion[]
fairy_souls: FairySouls
inventories: Inventories
@@ -61,6 +62,10 @@ export async function cleanSkyBlockProfileMemberResponse(member, included: Inclu
purse: member.coin_purse,
stats: cleanProfileStats(member),
+
+ // this is used for leaderboards
+ rawHypixelStats: member.stats ?? {},
+
minions: cleanMinions(member),
fairy_souls: cleanFairySouls(member),
inventories: inventoriesIncluded ? await cleanInventories(member) : undefined,
@@ -83,5 +88,5 @@ export interface CleanMemberProfilePlayer extends CleanPlayer {
export interface CleanMemberProfile {
member: CleanMemberProfilePlayer
- profile: CleanFullProfile
+ profile: CleanFullProfileBasicMembers
}
diff --git a/src/cleaners/skyblock/profile.ts b/src/cleaners/skyblock/profile.ts
index 2b092a1..6e98f8f 100644
--- a/src/cleaners/skyblock/profile.ts
+++ b/src/cleaners/skyblock/profile.ts
@@ -8,7 +8,14 @@ export interface CleanProfile extends CleanBasicProfile {
}
export interface CleanFullProfile extends CleanProfile {
- members: (CleanMember|CleanBasicMember)[]
+ members: CleanMember[]
+ bank: Bank
+ minions: CleanMinion[]
+ minion_count: number
+}
+
+export interface CleanFullProfileBasicMembers extends CleanProfile {
+ members: CleanBasicMember[]
bank: Bank
minions: CleanMinion[]
minion_count: number
@@ -38,19 +45,21 @@ 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, { mainMemberUuid }: ApiOptions): Promise<CleanFullProfile> {
- const cleanedMembers: CleanMember[] = []
-
+export async function cleanSkyblockProfileResponse(data: any, options?: ApiOptions): Promise<CleanFullProfile> {
+ // 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>[] = []
+
for (const memberUUID in data.members) {
const memberRaw = data.members[memberUUID]
memberRaw.uuid = memberUUID
- const member: CleanMember = await cleanSkyBlockProfileMemberResponse(
+ promises.push(cleanSkyBlockProfileMemberResponse(
memberRaw,
- ['stats', mainMemberUuid === memberUUID ? 'inventories' : undefined]
- )
- cleanedMembers.push(member)
+ ['stats', options?.mainMemberUuid === memberUUID ? 'inventories' : undefined]
+ ))
}
+ const cleanedMembers: CleanMember[] = await Promise.all(promises)
+
const memberMinions: CleanMinion[][] = []
for (const member of cleanedMembers) {
diff --git a/src/cleaners/skyblock/profiles.ts b/src/cleaners/skyblock/profiles.ts
index ea290f6..c9f5628 100644
--- a/src/cleaners/skyblock/profiles.ts
+++ b/src/cleaners/skyblock/profiles.ts
@@ -1,5 +1,5 @@
import { HypixelPlayerStatsSkyBlockProfiles } from "../../hypixelApi"
-import { CleanBasicProfile, CleanProfile, cleanSkyblockProfileResponseLighter } from "./profile"
+import { CleanBasicProfile, CleanProfile, cleanSkyblockProfileResponse, cleanSkyblockProfileResponseLighter } from "./profile"
export function cleanPlayerSkyblockProfiles(rawProfiles: HypixelPlayerStatsSkyBlockProfiles): CleanBasicProfile[] {
let profiles: CleanBasicProfile[] = []
@@ -16,7 +16,8 @@ export function cleanPlayerSkyblockProfiles(rawProfiles: HypixelPlayerStatsSkyBl
export async function cleanSkyblockProfilesResponse(data: any[]): Promise<CleanProfile[]> {
const cleanedProfiles: CleanProfile[] = []
for (const profile of data ?? []) {
- let cleanedProfile = await cleanSkyblockProfileResponseLighter(profile)
+ // let cleanedProfile = await cleanSkyblockProfileResponseLighter(profile)
+ let cleanedProfile = await cleanSkyblockProfileResponse(profile)
cleanedProfiles.push(cleanedProfile)
}
return cleanedProfiles
diff --git a/src/constants.ts b/src/constants.ts
new file mode 100644
index 0000000..c7581ae
--- /dev/null
+++ b/src/constants.ts
@@ -0,0 +1,172 @@
+/**
+ * Fetch and edit constants from the skyblock-constants repo
+ */
+
+import fetch from 'node-fetch'
+import { Agent } from 'https'
+import NodeCache from 'node-cache'
+
+const httpsAgent = new 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: string, route: string, headers?: any, json?: any) {
+ return await fetch(
+ githubApiBase + route,
+ {
+ agent: () => httpsAgent,
+ body: json ? JSON.stringify(json) : null,
+ method,
+ headers: Object.assign({
+ 'Authorization': `token ${process.env.github_token}`
+ }, headers),
+ }
+ )
+}
+
+interface GithubFile {
+ path: string
+ content: string
+ sha: string
+}
+
+// cache files for a day
+const fileCache = new NodeCache({
+ 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: string): Promise<GithubFile> {
+ 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: GithubFile, message: string, newContent: string) {
+ 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 */
+export async function fetchStats(): Promise<string[]> {
+ const file = await fetchFile('stats.json')
+ try {
+ return JSON.parse(file.content)
+ } catch {
+ // probably invalid json, return an empty array
+ return []
+ }
+}
+
+/** Fetch all the known SkyBlock collections as an array of strings */
+export async function fetchCollections(): Promise<string[]> {
+ const file = await fetchFile('collections.json')
+ try {
+ return JSON.parse(file.content)
+ } catch {
+ // probably invalid json, return an empty array
+ return []
+ }
+}
+
+/** Add stats to skyblock-constants. This has caching so it's fine to call many times */
+export async function addStats(addingStats: string[]) {
+ if (addingStats.length === 0) return // no stats provided, just return
+
+ const file = await fetchFile('stats.json')
+ if (!file.path)
+ return
+ let oldStats: string[]
+ 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))
+}
+
+/** Add stats to skyblock-constants. This has caching so it's fine to call many times */
+export async function addCollections(addingCollections: string[]) {
+ if (addingCollections.length === 0) return // no stats provided, just return
+
+ const file = await fetchFile('collections.json')
+ if (!file.path)
+ return
+ let oldCollections: string[]
+ try {
+ oldCollections = JSON.parse(file.content)
+ } catch {
+ // invalid json, set it as an empty array
+ oldCollections = []
+ }
+ const updatedCollections = oldCollections
+ .concat(addingCollections)
+ // remove duplicates
+ .filter((value, index, array) => array.indexOf(value) === index)
+ .sort((a, b) => a.localeCompare(b))
+ const newCollections = updatedCollections.filter(value => !oldCollections.includes(value))
+
+ // there's not actually any new stats, just return
+ if (newCollections.length === 0) return
+
+ const commitMessage = newCollections.length >= 2 ? `Add ${newCollections.length} new collections` : `Add '${newCollections[0]}'`
+
+ await editFile(file, commitMessage, JSON.stringify(updatedCollections, null, 2))
+}
diff --git a/src/database.ts b/src/database.ts
new file mode 100644
index 0000000..a1178bc
--- /dev/null
+++ b/src/database.ts
@@ -0,0 +1,173 @@
+/**
+ * Store data about members for leaderboards
+*/
+
+import * as constants from './constants'
+import * as cached from './hypixelCached'
+import { Collection, Db, FilterQuery, MongoClient } from 'mongodb'
+import NodeCache from 'node-cache'
+import { CleanMember } from './cleaners/skyblock/member'
+
+// don't update the user for 3 minutes
+const recentlyUpdated = new NodeCache({
+ stdTTL: 60 * 3,
+ checkperiod: 60,
+ useClones: false,
+})
+
+interface LeaderboardItem {
+ uuid: string
+ stats: any
+ last_updated: Date
+}
+
+const cachedLeaderboards: Map<string, any> = new Map()
+
+
+let client: MongoClient
+let database: Db
+let memberLeaderboardsCollection: Collection<LeaderboardItem>
+
+async function connect() {
+ if (!process.env.db_uri)
+ return console.warn('Warning: db_uri was not found in .env. Features that utilize the database such as leaderboards won\'t work.')
+ if (!process.env.db_name)
+ return console.warn('Warning: db_name was not found in .env. Features that utilize the database such as leaderboards won\'t work.')
+ client = await MongoClient.connect(process.env.db_uri, { useNewUrlParser: true, useUnifiedTopology: true })
+ database = client.db(process.env.db_name)
+ memberLeaderboardsCollection = database.collection('member-leaderboards')
+}
+
+
+function getMemberCollectionAttributes(member: CleanMember) {
+ const collectionAttributes = {}
+ for (const collection of member.collections) {
+ const collectionLeaderboardName = `collection_${collection.name}`
+ collectionAttributes[collectionLeaderboardName] = collection.xp
+ }
+ return collectionAttributes
+}
+
+function getMemberLeaderboardAttributes(member: CleanMember) {
+ // if you want to add a new leaderboard for member attributes, add it here (and getAllLeaderboardAttributes)
+ return {
+ // we use the raw stat names rather than the clean stats in case hypixel adds a new stat and it takes a while for us to clean it
+ ...member.rawHypixelStats,
+
+ // collection leaderboards
+ ...getMemberCollectionAttributes(member),
+
+ fairy_souls: member.fairy_souls.total,
+ first_join: member.first_join,
+ purse: member.purse,
+ visited_zones: member.visited_zones.length,
+ }
+}
+
+/** Fetch the names of all the leaderboards */
+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
+ ...await constants.fetchStats(),
+
+ // collection leaderboards
+ ...(await constants.fetchCollections()).map(value => `collection_${value}`),
+
+ 'fairy_souls',
+ 'first_join',
+ 'purse',
+ 'visited_zones',
+ ]
+}
+
+export async function fetchMemberLeaderboard(name: string) {
+ 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: FilterQuery<any> = {}
+ query[`stats.${name}`] = { '$exists': true }
+
+ const sortQuery: any = {}
+ sortQuery[`stats.${name}`] = -1
+
+
+ const leaderboardRaw = await memberLeaderboardsCollection.find(query).sort(sortQuery).limit(100).toArray()
+ const fetchLeaderboardPlayer = async(item: LeaderboardItem) => {
+ return {
+ player: await cached.fetchPlayer(item.uuid),
+ value: item.stats[name]
+ }
+ }
+ const promises = []
+ for (const item of leaderboardRaw) {
+ promises.push(fetchLeaderboardPlayer(item))
+ }
+ const leaderboard = await Promise.all(promises)
+ cachedLeaderboards.set(name, leaderboard)
+ return leaderboard
+}
+
+async function getMemberLeaderboardRequirement(name: string): Promise<LeaderboardItem> {
+ 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[99].value
+ else
+ return null
+}
+
+/** Update the member's leaderboard data on the server if applicable */
+export async function updateDatabaseMember(member: CleanMember) {
+ 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))
+ await constants.addCollections(member.collections.map(value => value.name))
+
+ const leaderboardAttributes = getMemberLeaderboardAttributes(member)
+
+ await memberLeaderboardsCollection.updateOne({
+ uuid: member.uuid
+ }, {
+ '$set': {
+ 'stats': leaderboardAttributes,
+ 'last_updated': new Date()
+ }
+ }, {
+ upsert: true
+ })
+}
+
+
+/**
+ * Remove leaderboard attributes for members that wouldn't actually be on the leaderboard. This saves a lot of storage space
+ */
+async function removeBadMemberLeaderboardAttributes() {
+ const leaderboards = await fetchAllMemberLeaderboardAttributes()
+ for (const leaderboard of leaderboards) {
+ // wait 10 seconds so it doesnt use as much ram
+ await new Promise(resolve => setTimeout(resolve, 10000))
+
+ const unsetValue = {}
+ unsetValue[leaderboard] = ''
+ const filter = {}
+ const requirement = await getMemberLeaderboardRequirement(leaderboard)
+ if (requirement !== null) {
+ filter[`stats.${leaderboard}`] = {
+ '$lt': requirement
+ }
+ await memberLeaderboardsCollection.updateMany(
+ filter,
+ { '$unset': unsetValue }
+ )
+ }
+ }
+}
+
+
+connect()
+ .then(removeBadMemberLeaderboardAttributes) \ No newline at end of file
diff --git a/src/hypixel.ts b/src/hypixel.ts
index 83ad419..3b8a952 100644
--- a/src/hypixel.ts
+++ b/src/hypixel.ts
@@ -6,9 +6,10 @@ import { CleanPlayer, cleanPlayerResponse } from './cleaners/player'
import { chooseApiKey, HypixelResponse, sendApiRequest } from './hypixelApi'
import * as cached from './hypixelCached'
import { CleanBasicMember, CleanMemberProfile } from './cleaners/skyblock/member'
-import { cleanSkyblockProfileResponse, CleanProfile, CleanBasicProfile } from './cleaners/skyblock/profile'
+import { cleanSkyblockProfileResponse, CleanProfile, CleanBasicProfile, CleanFullProfile, CleanFullProfileBasicMembers } from './cleaners/skyblock/profile'
import { cleanSkyblockProfilesResponse } from './cleaners/skyblock/profiles'
import { debug } from '.'
+import { updateDatabaseMember } from './database'
export type Included = 'profiles' | 'player' | 'stats' | 'inventories'
@@ -23,46 +24,47 @@ export const maxMinion = 11
*/
export interface ApiOptions {
- mainMemberUuid?: string
+ mainMemberUuid?: string
}
+/** Sends an API request to Hypixel and cleans it up. */
export async function sendCleanApiRequest({ path, args }, included?: Included[], options?: ApiOptions) {
- const key = await chooseApiKey()
- const rawResponse = await sendApiRequest({ path, key, args })
- if (rawResponse.throttled) {
+ const key = await chooseApiKey()
+ const rawResponse = await sendApiRequest({ path, key, args })
+ if (rawResponse.throttled) {
// if it's throttled, wait a second and try again
await new Promise(resolve => setTimeout(resolve, 1000))
- return await sendCleanApiRequest({ path, args }, included, options)
- }
+ return await sendCleanApiRequest({ path, args }, included, options)
+ }
- // clean the response
- return await cleanResponse({ path, data: rawResponse }, options ?? {})
+ // clean the response
+ return await cleanResponse({ path, data: rawResponse }, options ?? {})
}
async function cleanResponse({ path, data }: { path: string, data: HypixelResponse }, options: ApiOptions) {
- // Cleans up an api response
- switch (path) {
- case 'player': return await cleanPlayerResponse(data.player)
- case 'skyblock/profile': return await cleanSkyblockProfileResponse(data.profile, options)
- case 'skyblock/profiles': return await cleanSkyblockProfilesResponse(data.profiles)
- }
+ // Cleans up an api response
+ switch (path) {
+ case 'player': return await cleanPlayerResponse(data.player)
+ case 'skyblock/profile': return await cleanSkyblockProfileResponse(data.profile, options)
+ case 'skyblock/profiles': return await cleanSkyblockProfilesResponse(data.profiles)
+ }
}
/* ----------------------------- */
export interface UserAny {
- user?: string
- uuid?: string
- username?: string
+ user?: string
+ uuid?: string
+ username?: string
}
export interface CleanUser {
- player: CleanPlayer
- profiles?: CleanProfile[]
- activeProfile?: string
- online?: boolean
+ player: CleanPlayer
+ profiles?: CleanProfile[]
+ activeProfile?: string
+ online?: boolean
}
@@ -73,52 +75,52 @@ export interface CleanUser {
* used inclusions: player, profiles
*/
export async function fetchUser({ user, uuid, username }: UserAny, included: Included[]=['player']): Promise<CleanUser> {
- if (!uuid) {
- // If the uuid isn't provided, get it
- uuid = await cached.uuidFromUser(user || username)
- }
- if (!uuid) {
- // the user doesn't exist.
- if (debug) console.log('error:', user, 'doesnt exist')
- return null
- }
-
- const includePlayers = included.includes('player')
- const includeProfiles = included.includes('profiles')
-
- let profilesData: CleanProfile[]
- let basicProfilesData: CleanBasicProfile[]
- let playerData: CleanPlayer
-
- 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: CleanProfile = null
- let lastOnline: number = 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() - saveInterval): undefined
- }
+ if (!uuid) {
+ // If the uuid isn't provided, get it
+ uuid = await cached.uuidFromUser(user || username)
+ }
+ if (!uuid) {
+ // the user doesn't exist.
+ if (debug) console.log('error:', user, 'doesnt exist')
+ return null
+ }
+
+ const includePlayers = included.includes('player')
+ const includeProfiles = included.includes('profiles')
+
+ let profilesData: CleanProfile[]
+ let basicProfilesData: CleanBasicProfile[]
+ let playerData: CleanPlayer
+
+ 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: CleanProfile = null
+ let lastOnline: number = 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() - saveInterval): undefined
+ }
}
/**
@@ -128,40 +130,78 @@ export async function fetchUser({ user, uuid, username }: UserAny, included: Inc
* @param profile A profile name or profile uuid
*/
export async function fetchMemberProfile(user: string, profile: string): Promise<CleanMemberProfile> {
- const playerUuid = await cached.uuidFromUser(user)
- const profileUuid = await cached.fetchProfileUuid(user, profile)
+ const playerUuid = await cached.uuidFromUser(user)
+ const profileUuid = await cached.fetchProfileUuid(user, profile)
- // if the profile doesn't have an id, just return
- if (!profileUuid) return null
+ // if the profile doesn't have an id, just return
+ if (!profileUuid) return null
- const player = await cached.fetchPlayer(playerUuid)
+ const player = await cached.fetchPlayer(playerUuid)
- const cleanProfile = await cached.fetchProfile(playerUuid, profileUuid)
+ const cleanProfile = await cached.fetchProfile(playerUuid, profileUuid) as CleanFullProfileBasicMembers
- const member = cleanProfile.members.find(m => m.uuid === playerUuid)
+ const member = cleanProfile.members.find(m => m.uuid === playerUuid)
- // remove unnecessary member data
- const simpleMembers: CleanBasicMember[] = cleanProfile.members.map(m => {
- return {
- uuid: m.uuid,
- username: m.username,
- first_join: m.first_join,
- last_save: m.last_save,
- rank: m.rank
- }
- })
+ // remove unnecessary member data
+ const simpleMembers: CleanBasicMember[] = cleanProfile.members.map(m => {
+ return {
+ uuid: m.uuid,
+ username: m.username,
+ first_join: m.first_join,
+ last_save: m.last_save,
+ rank: m.rank
+ }
+ })
- cleanProfile.members = simpleMembers
+ cleanProfile.members = simpleMembers
- return {
- member: {
+ return {
+ member: {
// the profile name is in member rather than profile since they sometimes differ for each member
- profileName: cleanProfile.name,
+ profileName: cleanProfile.name,
// add all the member data
- ...member,
- // add all other data relating to the hypixel player, such as username, rank, etc
- ...player
- },
- profile: cleanProfile
- }
+ ...member,
+ // add all other data relating to the hypixel player, such as username, rank, etc
+ ...player
+ },
+ profile: cleanProfile
+ }
}
+
+/**
+ * Fetches the Hypixel API to get a CleanFullProfile. This doesn't do any caching and you should use hypixelCached.fetchProfile instead
+ * @param playerUuid The UUID of the Minecraft player
+ * @param profileUuid The UUID of the Hypixel SkyBlock profile
+ */
+export async function fetchMemberProfileUncached(playerUuid: string, profileUuid: string): Promise<CleanFullProfile> {
+ const profile: CleanFullProfile = await sendCleanApiRequest(
+ {
+ path: 'skyblock/profile',
+ args: { profile: profileUuid }
+ },
+ null,
+ { mainMemberUuid: playerUuid }
+ )
+ for (const member of profile.members)
+ updateDatabaseMember(member)
+ return profile
+}
+
+
+export async function fetchMemberProfilesUncached(playerUuid: string): Promise<CleanFullProfile[]> {
+ const profiles: CleanFullProfile[] = 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)
+ updateDatabaseMember(member)
+ return profiles
+} \ No newline at end of file
diff --git a/src/hypixelCached.ts b/src/hypixelCached.ts
index 1b8b617..83360a9 100644
--- a/src/hypixelCached.ts
+++ b/src/hypixelCached.ts
@@ -2,7 +2,7 @@
* Fetch the clean and cached Hypixel API
*/
-import NodeCache from 'node-cache'
+import NodeCache, { EventEmitter, Key } from 'node-cache'
import * as mojang from './mojang'
import * as hypixel from './hypixel'
import { CleanPlayer } from './cleaners/player'
@@ -10,8 +10,6 @@ import { undashUuid } from './util'
import { CleanProfile, CleanFullProfile, CleanBasicProfile } from './cleaners/skyblock/profile'
import { debug } from '.'
-
-
// cache usernames for 4 hours
const usernameCache = new NodeCache({
stdTTL: 60 * 60 * 4,
@@ -49,28 +47,54 @@ const profileNameCache = new NodeCache({
useClones: false,
})
+function waitForSet(cache: NodeCache, key?: string, value?: string): Promise<any> {
+ 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
*/
export async function uuidFromUser(user: string): Promise<string> {
- if (usernameCache.has(undashUuid(user)))
+ if (usernameCache.has(undashUuid(user))) {
// check if the uuid is a key
- return undashUuid(user)
+ const username: any = usernameCache.get(undashUuid(user))
+ // if it has .then, then that means its a waitForSet promise. This is done to prevent requests made while it is already requesting
+ if (username.then) {
+ return (await username()).key
+ } else
+ return undashUuid(user)
+ }
// check if the username is a value
const uuidToUsername: {[ key: string ]: string} = usernameCache.mget(usernameCache.keys())
for (const [ uuid, username ] of Object.entries(uuidToUsername)) {
- if (user.toLowerCase() === username.toLowerCase())
+ if (username.toLowerCase && user.toLowerCase() === username.toLowerCase())
return uuid
}
+ if (debug) console.log('Cache miss: uuidFromUser', user)
+
+ // set it as waitForSet (a promise) in case uuidFromUser gets called while its fetching mojang
+ usernameCache.set(undashUuid(user), waitForSet(usernameCache, user, 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 = undashUuid(uuid)
+
+ if (user !== uuid) usernameCache.del(user)
+
usernameCache.set(uuid, username)
return uuid
}
@@ -85,6 +109,8 @@ export async function usernameFromUser(user: string): Promise<string> {
return usernameCache.get(undashUuid(user))
}
+ if (debug) console.log('Cache miss: usernameFromUser', user)
+
let { uuid, username } = await mojang.mojangDataFromUser(user)
uuid = undashUuid(uuid)
usernameCache.set(uuid, username)
@@ -96,7 +122,6 @@ export async function fetchPlayer(user: string): Promise<CleanPlayer> {
const playerUuid = await uuidFromUser(user)
if (playerCache.has(playerUuid)) {
- if (debug) console.log('Cache hit! fetchPlayer', playerUuid)
return playerCache.get(playerUuid)
}
@@ -120,17 +145,9 @@ export async function fetchSkyblockProfiles(playerUuid: string): Promise<CleanPr
return profilesCache.get(playerUuid)
}
- const profiles: CleanFullProfile[] = 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 (debug) console.log('Cache miss: fetchSkyblockProfiles', playerUuid)
+
+ const profiles: CleanProfile[] = await hypixel.fetchMemberProfilesUncached(playerUuid)
const basicProfiles: CleanProfile[] = []
@@ -165,6 +182,9 @@ async function fetchBasicProfiles(user: string): Promise<CleanBasicProfile[]> {
if (debug) console.log('Cache hit! fetchBasicProfiles', playerUuid)
return basicProfilesCache.get(playerUuid)
}
+
+ if (debug) console.log('Cache miss: fetchBasicProfiles', user)
+
const player = await fetchPlayer(playerUuid)
const profiles = player.profiles
basicProfilesCache.set(playerUuid, profiles)
@@ -188,6 +208,8 @@ export async function fetchProfileUuid(user: string, profile: string) {
return null
}
+ if (debug) console.log('Cache miss: fetchProfileUuid', user)
+
const profiles = await fetchBasicProfiles(user)
const profileUuid = undashUuid(profile)
@@ -215,16 +237,11 @@ export async function fetchProfile(user: string, profile: string): Promise<Clean
return profileCache.get(profileUuid)
}
+ if (debug) console.log('Cache miss: fetchProfile', user, profile)
+
const profileName = await fetchProfileName(user, profile)
- const cleanProfile: CleanFullProfile = await hypixel.sendCleanApiRequest(
- {
- path: 'skyblock/profile',
- args: { profile: profileUuid }
- },
- null,
- { mainMemberUuid: playerUuid }
- )
+ const cleanProfile: CleanFullProfile = await hypixel.fetchMemberProfileUncached(playerUuid, profileUuid)
// we know the name from fetchProfileName, so set it here
cleanProfile.name = profileName
@@ -250,6 +267,8 @@ export async function fetchProfileName(user: string, profile: string): Promise<s
return profileNameCache.get(`${playerUuid}.${profileUuid}`)
}
+ if (debug) console.log('Cache miss: fetchProfileName', user, profile)
+
const basicProfiles = await fetchBasicProfiles(playerUuid)
let profileName
for (const basicProfile of basicProfiles)
diff --git a/src/index.ts b/src/index.ts
index adeab57..0c33930 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,6 @@
import { fetchMemberProfile, fetchUser } from './hypixel'
import express from 'express'
+import { fetchMemberLeaderboard } from './database'
const app = express()
@@ -33,4 +34,11 @@ app.get('/player/:user/:profile', async(req, res) => {
)
})
+app.get('/leaderboard/:name', async(req, res) => {
+ res.json(
+ await fetchMemberLeaderboard(req.params.name)
+ )
+})
+
+
app.listen(8080, () => console.log('App started :)'))
diff --git a/src/util.ts b/src/util.ts
index e9fa145..2ff55a8 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -3,7 +3,7 @@
*/
export function undashUuid(uuid: string): string {
- return uuid.replace(/-/g, '')
+ return uuid.replace(/-/g, '').toLowerCase()
}