diff options
-rw-r--r-- | build/cleaners/skyblock/member.js | 3 | ||||
-rw-r--r-- | build/cleaners/skyblock/profile.js | 4 | ||||
-rw-r--r-- | build/cleaners/skyblock/profiles.js | 3 | ||||
-rw-r--r-- | build/constants.js | 111 | ||||
-rw-r--r-- | build/database.js | 115 | ||||
-rw-r--r-- | build/hypixel.js | 35 | ||||
-rw-r--r-- | build/hypixelCached.js | 62 | ||||
-rw-r--r-- | build/index.js | 2 | ||||
-rw-r--r-- | build/util.js | 2 | ||||
-rw-r--r-- | package-lock.json | 1051 | ||||
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | src/cleaners/rank.ts | 84 | ||||
-rw-r--r-- | src/cleaners/skyblock/member.ts | 9 | ||||
-rw-r--r-- | src/cleaners/skyblock/profile.ts | 25 | ||||
-rw-r--r-- | src/cleaners/skyblock/profiles.ts | 5 | ||||
-rw-r--r-- | src/constants.ts | 172 | ||||
-rw-r--r-- | src/database.ts | 173 | ||||
-rw-r--r-- | src/hypixel.ts | 230 | ||||
-rw-r--r-- | src/hypixelCached.ts | 71 | ||||
-rw-r--r-- | src/index.ts | 8 | ||||
-rw-r--r-- | src/util.ts | 2 |
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() } |