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="
+ },
+ "