diff options
author | mat <github@matdoes.dev> | 2021-06-29 17:54:50 -0500 |
---|---|---|
committer | mat <github@matdoes.dev> | 2021-06-29 17:54:50 -0500 |
commit | 49ff24343e1c964b6cb82fdf76957ffd2f27d049 (patch) | |
tree | 820a3668e21dffc7eefa53630944412d9d97a129 | |
parent | 758ca15277daa11ce1ec86ce7f07ea7beee9eb8f (diff) | |
parent | c0c534dafb54ebf9f95a5054f576ad99de29f232 (diff) | |
download | skyblock-api-49ff24343e1c964b6cb82fdf76957ffd2f27d049.tar.gz skyblock-api-49ff24343e1c964b6cb82fdf76957ffd2f27d049.tar.bz2 skyblock-api-49ff24343e1c964b6cb82fdf76957ffd2f27d049.zip |
Merge branch 'main' of https://github.com/skyblockstats/skyblock-api into main
30 files changed, 278 insertions, 169 deletions
diff --git a/build/cleaners/player.js b/build/cleaners/player.js index dadb129..42c76b8 100644 --- a/build/cleaners/player.js +++ b/build/cleaners/player.js @@ -9,7 +9,7 @@ async function cleanPlayerResponse(data) { var _a, _b; // Cleans up a 'player' api response if (!data) - return; // bruh + return null; // bruh return { uuid: util_1.undashUuid(data.uuid), username: data.displayname, diff --git a/build/cleaners/skyblock/member.js b/build/cleaners/skyblock/member.js index 8e72be8..cc66488 100644 --- a/build/cleaners/skyblock/member.js +++ b/build/cleaners/skyblock/member.js @@ -31,8 +31,10 @@ const zones_1 = require("./zones"); const skills_1 = require("./skills"); const cached = __importStar(require("../../hypixelCached")); const constants = __importStar(require("../../constants")); -async function cleanSkyBlockProfileMemberResponseBasic(member, included = null) { +async function cleanSkyBlockProfileMemberResponseBasic(member) { const player = await cached.fetchPlayer(member.uuid); + if (!player) + return null; return { uuid: member.uuid, username: player.username, @@ -43,13 +45,13 @@ async function cleanSkyBlockProfileMemberResponseBasic(member, included = null) } exports.cleanSkyBlockProfileMemberResponseBasic = cleanSkyBlockProfileMemberResponseBasic; /** Cleans up a member (from skyblock/profile) */ -async function cleanSkyBlockProfileMemberResponse(member, included = null) { +async function cleanSkyBlockProfileMemberResponse(member, included = undefined) { var _a; // profiles.members[] - const inventoriesIncluded = included === null || included.includes('inventories'); + const inventoriesIncluded = included === undefined || included.includes('inventories'); const player = await cached.fetchPlayer(member.uuid); if (!player) - return; + return null; const fairySouls = fairysouls_1.cleanFairySouls(member); const { max_fairy_souls: maxFairySouls } = await constants.fetchConstantValues(); if (fairySouls.total > (maxFairySouls !== null && maxFairySouls !== void 0 ? maxFairySouls : 0)) diff --git a/build/cleaners/skyblock/minions.js b/build/cleaners/skyblock/minions.js index fae3c96..d2bfc41 100644 --- a/build/cleaners/skyblock/minions.js +++ b/build/cleaners/skyblock/minions.js @@ -89,7 +89,7 @@ function combineMinionArrays(minions) { // This should never happen, but in case the length of `minion.levels` is longer than // `matchingMinionReference.levels`, then it should be extended to be equal length while (matchingMinionReference.levels.length < minion.levels.length) - matchingMinionReference.levels.push(null); + matchingMinionReference.levels.push(false); for (let i = 0; i < minion.levels.length; i++) { if (minion.levels[i]) matchingMinionReference.levels[i] = true; diff --git a/build/cleaners/skyblock/profile.js b/build/cleaners/skyblock/profile.js index 45ef57b..da988cc 100644 --- a/build/cleaners/skyblock/profile.js +++ b/build/cleaners/skyblock/profile.js @@ -34,7 +34,7 @@ async function cleanSkyblockProfileResponseLighter(data) { // we pass an empty array to make it not check stats promises.push(member_1.cleanSkyBlockProfileMemberResponseBasic(memberRaw)); } - const cleanedMembers = await Promise.all(promises); + const cleanedMembers = (await Promise.all(promises)).filter(m => m); return { uuid: data.profile_id, name: data.cute_name, diff --git a/build/cleaners/skyblock/stats.js b/build/cleaners/skyblock/stats.js index 482771c..4ca671b 100644 --- a/build/cleaners/skyblock/stats.js +++ b/build/cleaners/skyblock/stats.js @@ -74,6 +74,7 @@ function getStatUnit(name) { return unitName; } } + return null; } exports.getStatUnit = getStatUnit; function cleanProfileStats(data) { diff --git a/build/constants.js b/build/constants.js index 22327c5..414a6c5 100644 --- a/build/constants.js +++ b/build/constants.js @@ -57,7 +57,7 @@ async function fetchGithubApi(method, route, headers, json) { console.debug('fetching github api', method, route); const data = await node_fetch_1.default(githubApiBase + route, { agent: () => httpsAgent, - body: json ? JSON.stringify(json) : null, + body: json ? JSON.stringify(json) : undefined, method, headers: Object.assign({ 'Authorization': `token ${process.env.github_token}` diff --git a/build/database.js b/build/database.js index eb5a63f..c34222d 100644 --- a/build/database.js +++ b/build/database.js @@ -137,9 +137,11 @@ async function fetchAllLeaderboardsCategorized() { const categorizedLeaderboards = {}; for (const leaderboard of [...memberLeaderboardAttributes, ...profileLeaderboardAttributes]) { const { category } = stats_1.categorizeStat(leaderboard); - if (!categorizedLeaderboards[category]) - categorizedLeaderboards[category] = []; - categorizedLeaderboards[category].push(leaderboard); + if (category) { + if (!categorizedLeaderboards[category]) + categorizedLeaderboards[category] = []; + categorizedLeaderboards[category].push(leaderboard); + } } // move misc to end by removing and readding it const misc = categorizedLeaderboards.misc; @@ -277,8 +279,9 @@ async function fetchMemberLeaderboard(name) { var _a; const leaderboardRaw = await fetchMemberLeaderboardRaw(name); const fetchLeaderboardPlayer = async (i) => { + const player = await cached.fetchBasicPlayer(i.uuid); return { - player: await cached.fetchBasicPlayer(i.uuid), + player, profileUuid: i.profile, value: i.value }; @@ -301,8 +304,11 @@ async function fetchProfileLeaderboard(name) { const leaderboardRaw = await fetchProfileLeaderboardRaw(name); const fetchLeaderboardProfile = async (i) => { const players = []; - for (const playerUuid of i.players) - players.push(await cached.fetchBasicPlayer(playerUuid)); + for (const playerUuid of i.players) { + const player = await cached.fetchBasicPlayer(playerUuid); + if (player) + players.push(player); + } return { players: players, profileUuid: i.uuid, @@ -340,7 +346,11 @@ exports.fetchLeaderboard = fetchLeaderboard; async function fetchMemberLeaderboardSpots(player, profile) { var _a; const fullProfile = await cached.fetchProfile(player, profile); + if (!fullProfile) + return null; const fullMember = fullProfile.members.find(m => m.username.toLowerCase() === player.toLowerCase() || m.uuid === player); + if (!fullMember) + return null; // update the leaderboard positions for the member await updateDatabaseMember(fullMember, fullProfile); const applicableAttributes = await getApplicableMemberLeaderboardAttributes(fullMember); @@ -404,8 +414,8 @@ async function getApplicableProfileLeaderboardAttributes(profile) { } let leaderboardsCount = Object.keys(applicableAttributes).length; const leaderboardsCountRequirement = await getLeaderboardRequirement('leaderboards_count', 'member'); - if ((leaderboardsCountRequirement === null) - || (leaderboardsCount > leaderboardsCountRequirement)) { + if (leaderboardsCountRequirement === null + || leaderboardsCount > leaderboardsCountRequirement) { applicableAttributes['leaderboards_count'] = leaderboardsCount; } return applicableAttributes; @@ -423,7 +433,8 @@ async function updateDatabaseMember(member, profile) { recentlyUpdated.set(profile.uuid + member.uuid, true); if (_1.debug) console.debug('adding member to leaderboards', member.username); - await constants.addStats(Object.keys(member.rawHypixelStats)); + if (member.rawHypixelStats) + await constants.addStats(Object.keys(member.rawHypixelStats)); await constants.addCollections(member.collections.map(coll => coll.name)); await constants.addSkills(member.skills.map(skill => skill.name)); await constants.addZones(member.visited_zones.map(zone => zone.name)); diff --git a/build/discord.js b/build/discord.js index a92521a..ba9f3eb 100644 --- a/build/discord.js +++ b/build/discord.js @@ -13,6 +13,10 @@ const httpsAgent = new https_1.Agent({ async function exchangeCode(redirectUri, code) { const API_ENDPOINT = 'https://discord.com/api/v6'; const CLIENT_SECRET = process.env.discord_client_secret; + if (!CLIENT_SECRET) { + console.error('discord_client_secret isn\'t in env, couldn\'t login with discord'); + return null; + } const data = { 'client_id': DISCORD_CLIENT_ID, 'client_secret': CLIENT_SECRET, diff --git a/build/hypixel.js b/build/hypixel.js index 8750170..414139e 100644 --- a/build/hypixel.js +++ b/build/hypixel.js @@ -62,22 +62,25 @@ async function cleanResponse({ path, data }, options) { * used inclusions: player, profiles */ async function fetchUser({ user, uuid, username }, included = ['player'], customization) { + var _a, _b; if (!uuid) { // If the uuid isn't provided, get it - uuid = await cached.uuidFromUser(user || username); + if (!username && !user) + return null; + uuid = await cached.uuidFromUser((user !== null && user !== void 0 ? user : username)); } - const websiteAccountPromise = customization ? database_1.fetchAccount(uuid) : null; if (!uuid) { // the user doesn't exist. if (_1.debug) console.debug('error:', user, 'doesnt exist'); return null; } + const websiteAccountPromise = customization ? database_1.fetchAccount(uuid) : null; const includePlayers = included.includes('player'); const includeProfiles = included.includes('profiles'); let profilesData; let basicProfilesData; - let playerData; + let playerData = null; if (includePlayers) { playerData = await cached.fetchPlayer(uuid); // if not including profiles, include lightweight profiles just in case @@ -86,28 +89,26 @@ async function fetchUser({ user, uuid, username }, included = ['player'], custom if (playerData) delete playerData.profiles; } - if (includeProfiles) { + if (includeProfiles) profilesData = await cached.fetchSkyblockProfiles(uuid); - } - let activeProfile = null; + let activeProfile; let lastOnline = 0; if (includeProfiles) { for (const profile of profilesData) { - const member = profile.members.find(member => member.uuid === uuid); - if (member.last_save > lastOnline) { + const member = (_a = profile.members) === null || _a === void 0 ? void 0 : _a.find(member => member.uuid === uuid); + if (member && member.last_save > lastOnline) { lastOnline = member.last_save; activeProfile = profile; } } } - let websiteAccount = undefined; - if (websiteAccountPromise) { + let websiteAccount = null; + if (websiteAccountPromise) websiteAccount = await websiteAccountPromise; - } return { - player: playerData !== null && playerData !== void 0 ? playerData : null, + player: playerData, profiles: profilesData !== null && profilesData !== void 0 ? profilesData : basicProfilesData, - activeProfile: includeProfiles ? activeProfile === null || activeProfile === void 0 ? void 0 : activeProfile.uuid : undefined, + activeProfile: includeProfiles ? (_b = activeProfile) === null || _b === void 0 ? void 0 : _b.uuid : undefined, online: includeProfiles ? lastOnline > (Date.now() - exports.saveInterval) : undefined, customization: websiteAccount === null || websiteAccount === void 0 ? void 0 : websiteAccount.customization }; @@ -123,7 +124,7 @@ exports.fetchUser = fetchUser; async function fetchMemberProfile(user, profile, customization) { const playerUuid = await cached.uuidFromUser(user); if (!playerUuid) - return; + return null; // we don't await the promise immediately so it can load while we do other stuff const websiteAccountPromise = customization ? database_1.fetchAccount(playerUuid) : null; const profileUuid = await cached.fetchProfileUuid(user, profile); @@ -133,8 +134,12 @@ async function fetchMemberProfile(user, profile, customization) { if (!playerUuid) return null; const player = await cached.fetchPlayer(playerUuid); + if (!player) + return null; // this should never happen, but if it does just return null const cleanProfile = await cached.fetchProfile(playerUuid, profileUuid); const member = cleanProfile.members.find(m => m.uuid === playerUuid); + if (!member) + return null; // this should never happen, but if it does just return null // remove unnecessary member data const simpleMembers = cleanProfile.members.map(m => { return { @@ -146,7 +151,7 @@ async function fetchMemberProfile(user, profile, customization) { }; }); cleanProfile.members = simpleMembers; - let websiteAccount = undefined; + let websiteAccount = null; if (websiteAccountPromise) websiteAccount = await websiteAccountPromise; return { @@ -172,7 +177,7 @@ async function fetchMemberProfileUncached(playerUuid, profileUuid) { const profile = await sendCleanApiRequest({ path: 'skyblock/profile', args: { profile: profileUuid } - }, null, { mainMemberUuid: playerUuid }); + }, undefined, { mainMemberUuid: playerUuid }); // queue updating the leaderboard positions for the member, eventually for (const member of profile.members) database_1.queueUpdateDatabaseMember(member, profile); @@ -189,7 +194,7 @@ async function fetchBasicProfileFromUuidUncached(profileUuid) { const profile = await sendCleanApiRequest({ path: 'skyblock/profile', args: { profile: profileUuid } - }, null, { basic: true }); + }, undefined, { basic: true }); return profile; } exports.fetchBasicProfileFromUuidUncached = fetchBasicProfileFromUuidUncached; @@ -199,7 +204,7 @@ async function fetchMemberProfilesUncached(playerUuid) { args: { uuid: playerUuid } - }, null, { + }, undefined, { // only the inventories for the main player are generated, this is for optimization purposes mainMemberUuid: playerUuid }); diff --git a/build/hypixelApi.js b/build/hypixelApi.js index df6d426..a35dd64 100644 --- a/build/hypixelApi.js +++ b/build/hypixelApi.js @@ -60,6 +60,7 @@ exports.getKeyUsage = getKeyUsage; /** Send an HTTP request to the Hypixel API */ async function sendApiRequest({ path, key, args }) { // Send a raw http request to api.hypixel.net, and return the parsed json + var _a, _b, _c; if (key) // If there's an api key, add it to the arguments args.key = key; @@ -84,9 +85,9 @@ async function sendApiRequest({ path, key, args }) { if (fetchResponse.headers.get('ratelimit-limit')) // remember how many uses it has apiKeyUsage[key] = { - remaining: parseInt(fetchResponse.headers.get('ratelimit-remaining')), - limit: parseInt(fetchResponse.headers.get('ratelimit-limit')), - reset: Date.now() + parseInt(fetchResponse.headers.get('ratelimit-reset')) * 1000 + remaining: parseInt((_a = fetchResponse.headers.get('ratelimit-remaining')) !== null && _a !== void 0 ? _a : '0'), + limit: parseInt((_b = fetchResponse.headers.get('ratelimit-limit')) !== null && _b !== void 0 ? _b : '0'), + reset: Date.now() + parseInt((_c = fetchResponse.headers.get('ratelimit-reset')) !== null && _c !== void 0 ? _c : '0') * 1000 }; if (fetchJsonParsed.throttle) { if (apiKeyUsage[key]) diff --git a/build/hypixelCached.js b/build/hypixelCached.js index d732f2d..d54aa68 100644 --- a/build/hypixelCached.js +++ b/build/hypixelCached.js @@ -95,7 +95,7 @@ async function uuidFromUser(user) { const username = exports.usernameCache.get(util_1.undashUuid(user)); // sometimes the username will be null, return that if (username === null) - return null; + return undefined; // if it has .then, then that means its a waitForCacheSet promise. This is done to prevent requests made while it is already requesting if (username.then) { const { key: uuid, value: _username } = await username; @@ -134,14 +134,17 @@ exports.uuidFromUser = uuidFromUser; * @param user A user can be either a uuid or a username */ async function usernameFromUser(user) { + var _a; if (exports.usernameCache.has(util_1.undashUuid(user))) { if (_1.debug) console.debug('Cache hit! usernameFromUser', user); - return exports.usernameCache.get(util_1.undashUuid(user)); + return (_a = exports.usernameCache.get(util_1.undashUuid(user))) !== null && _a !== void 0 ? _a : null; } if (_1.debug) console.debug('Cache miss: usernameFromUser', user); let { uuid, username } = await mojang.profileFromUser(user); + if (!uuid) + return null; uuid = util_1.undashUuid(uuid); exports.usernameCache.set(uuid, username); return username; @@ -150,6 +153,8 @@ exports.usernameFromUser = usernameFromUser; let fetchingPlayers = new Set(); async function fetchPlayer(user) { const playerUuid = await uuidFromUser(user); + if (!playerUuid) + return null; if (exports.playerCache.has(playerUuid)) return exports.playerCache.get(playerUuid); // if it's already in the process of fetching, check every 100ms until it's not fetching the player anymore and fetch it again, since it'll be cached now @@ -166,7 +171,7 @@ async function fetchPlayer(user) { }); fetchingPlayers.delete(playerUuid); if (!cleanPlayer) - return; + return null; // clone in case it gets modified somehow later exports.playerCache.set(playerUuid, cleanPlayer); exports.usernameCache.set(playerUuid, cleanPlayer.username); @@ -179,16 +184,21 @@ exports.fetchPlayer = fetchPlayer; /** Fetch a player without their profiles. This is heavily cached. */ async function fetchBasicPlayer(user) { const playerUuid = await uuidFromUser(user); + if (!playerUuid) + return null; if (exports.basicPlayerCache.has(playerUuid)) return exports.basicPlayerCache.get(playerUuid); const player = await fetchPlayer(playerUuid); - if (!player) + if (!player) { console.debug('no player? this should never happen', user, playerUuid); + return null; + } delete player.profiles; return player; } exports.fetchBasicPlayer = fetchBasicPlayer; async function fetchSkyblockProfiles(playerUuid) { + var _a; if (exports.profilesCache.has(playerUuid)) { if (_1.debug) console.debug('Cache hit! fetchSkyblockProfiles', playerUuid); @@ -203,7 +213,7 @@ async function fetchSkyblockProfiles(playerUuid) { const basicProfile = { name: profile.name, uuid: profile.uuid, - members: profile.members.map(m => { + members: (_a = profile.members) === null || _a === void 0 ? void 0 : _a.map(m => { return { uuid: m.uuid, username: m.username, @@ -224,7 +234,7 @@ exports.fetchSkyblockProfiles = fetchSkyblockProfiles; async function fetchBasicProfiles(user) { const playerUuid = await uuidFromUser(user); if (!playerUuid) - return; // invalid player, just return + return null; // invalid player, just return if (exports.basicProfilesCache.has(playerUuid)) { if (_1.debug) console.debug('Cache hit! fetchBasicProfiles', playerUuid); @@ -239,6 +249,8 @@ async function fetchBasicProfiles(user) { } const profiles = player.profiles; exports.basicProfilesCache.set(playerUuid, profiles); + if (!profiles) + return null; // cache the profile names and uuids to profileNameCache because we can for (const profile of profiles) exports.profileNameCache.set(`${playerUuid}.${profile.uuid}`, profile.name); @@ -250,6 +262,7 @@ async function fetchBasicProfiles(user) { * @param profile A profile name or profile uuid */ async function fetchProfileUuid(user, profile) { + var _a; // if a profile wasn't provided, return if (!profile) { if (_1.debug) @@ -260,10 +273,10 @@ async function fetchProfileUuid(user, profile) { console.debug('Cache miss: fetchProfileUuid', user, profile); const profiles = await fetchBasicProfiles(user); if (!profiles) - return; // user probably doesnt exist + return null; // user probably doesnt exist const profileUuid = util_1.undashUuid(profile); for (const p of profiles) { - if (p.name.toLowerCase() === profileUuid.toLowerCase()) + if (((_a = p.name) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === profileUuid.toLowerCase()) return util_1.undashUuid(p.uuid); else if (util_1.undashUuid(p.uuid) === util_1.undashUuid(profileUuid)) return util_1.undashUuid(p.uuid); @@ -278,6 +291,8 @@ exports.fetchProfileUuid = fetchProfileUuid; */ async function fetchProfile(user, profile) { const playerUuid = await uuidFromUser(user); + if (!playerUuid) + return null; const profileUuid = await fetchProfileUuid(playerUuid, profile); if (!profileUuid) return null; @@ -290,6 +305,8 @@ async function fetchProfile(user, profile) { if (_1.debug) console.debug('Cache miss: fetchProfile', user, profile); const profileName = await fetchProfileName(user, profile); + if (!profileName) + return null; // uhh this should never happen but if it does just return null const cleanProfile = await hypixel.fetchMemberProfileUncached(playerUuid, profileUuid); // we know the name from fetchProfileName, so set it here cleanProfile.name = profileName; @@ -307,6 +324,8 @@ async function fetchBasicProfileFromUuid(profileUuid) { if (_1.debug) console.debug('Cache hit! fetchBasicProfileFromUuid', profileUuid); const profile = exports.profileCache.get(profileUuid); + if (!profile) + return undefined; return { uuid: profile.uuid, members: profile.members.map(m => ({ @@ -329,24 +348,29 @@ exports.fetchBasicProfileFromUuid = fetchBasicProfileFromUuid; * @param profile A profile uuid or name */ async function fetchProfileName(user, profile) { + var _a, _b; // we're fetching the profile and player uuid again in case we were given a name, but it's cached so it's not much of a problem const profileUuid = await fetchProfileUuid(user, profile); if (!profileUuid) return null; const playerUuid = await uuidFromUser(user); + if (!playerUuid) + return null; if (exports.profileNameCache.has(`${playerUuid}.${profileUuid}`)) { // Return the profile name if it's cached if (_1.debug) console.debug('Cache hit! fetchProfileName', profileUuid); - return exports.profileNameCache.get(`${playerUuid}.${profileUuid}`); + return (_a = exports.profileNameCache.get(`${playerUuid}.${profileUuid}`)) !== null && _a !== void 0 ? _a : null; } if (_1.debug) console.debug('Cache miss: fetchProfileName', user, profile); const basicProfiles = await fetchBasicProfiles(playerUuid); - let profileName; + if (!basicProfiles) + return null; + let profileName = null; for (const basicProfile of basicProfiles) if (basicProfile.uuid === playerUuid) - profileName = basicProfile.name; + profileName = (_b = basicProfile.name) !== null && _b !== void 0 ? _b : null; exports.profileNameCache.set(`${playerUuid}.${profileUuid}`, profileName); return profileName; } diff --git a/build/index.js b/build/index.js index 736b137..8bb2040 100644 --- a/build/index.js +++ b/build/index.js @@ -136,7 +136,12 @@ app.get('/constants', async (req, res) => { app.post('/accounts/createsession', async (req, res) => { try { const { code } = req.body; - const { access_token: accessToken, refresh_token: refreshToken } = await discord.exchangeCode(`${mainSiteUrl}/loggedin`, code); + const codeExchange = await discord.exchangeCode(`${mainSiteUrl}/loggedin`, code); + if (!codeExchange) { + res.json({ ok: false, error: 'discord_client_secret isn\'t in env' }); + return; + } + const { access_token: accessToken, refresh_token: refreshToken } = codeExchange; if (!accessToken) // access token is invalid :( return res.json({ ok: false }); @@ -152,6 +157,8 @@ app.post('/accounts/session', async (req, res) => { try { const { uuid } = req.body; const session = await database_1.fetchSession(uuid); + if (!session) + return res.json({ ok: false }); const account = await database_1.fetchAccountFromDiscord(session.discord_user.id); res.json({ session, account }); } diff --git a/src/cleaners/player.ts b/src/cleaners/player.ts index 6ac1382..b2dfc77 100644 --- a/src/cleaners/player.ts +++ b/src/cleaners/player.ts @@ -17,10 +17,10 @@ export interface CleanPlayer extends CleanBasicPlayer { // first_join?: number } -export async function cleanPlayerResponse(data: HypixelPlayer): Promise<CleanPlayer> { +export async function cleanPlayerResponse(data: HypixelPlayer): Promise<CleanPlayer | null> { // Cleans up a 'player' api response if (!data) - return // bruh + return null // bruh return { uuid: undashUuid(data.uuid), username: data.displayname, diff --git a/src/cleaners/skyblock/collections.ts b/src/cleaners/skyblock/collections.ts index 708822b..99fdc27 100644 --- a/src/cleaners/skyblock/collections.ts +++ b/src/cleaners/skyblock/collections.ts @@ -84,7 +84,7 @@ export interface Collection { } // get a category name (farming) from a collection name (wheat) -function getCategory(collectionName): CollectionCategory { +function getCategory(collectionName): CollectionCategory | undefined { for (const categoryName in COLLECTIONS) { const categoryItems = COLLECTIONS[categoryName] if (categoryItems.includes(collectionName)) diff --git a/src/cleaners/skyblock/inventory.ts b/src/cleaners/skyblock/inventory.ts index 890937a..5ad6617 100644 --- a/src/cleaners/skyblock/inventory.ts +++ b/src/cleaners/skyblock/inventory.ts @@ -26,7 +26,7 @@ interface Item { export type Inventory = Item[] -function cleanItem(rawItem): Item { +function cleanItem(rawItem): Item | null { // if the item doesn't have an id, it isn't an item if (rawItem.id === undefined) return null @@ -35,7 +35,7 @@ function cleanItem(rawItem): Item { const damageValue = rawItem.Damage const itemTag = rawItem.tag const extraAttributes = itemTag?.ExtraAttributes ?? {} - let headId: string + let headId: string | undefined if (vanillaId === 397) { const headDataBase64 = itemTag?.SkullOwner?.Properties?.textures?.[0]?.Value diff --git a/src/cleaners/skyblock/member.ts b/src/cleaners/skyblock/member.ts index 4a2f8cc..cee1864 100644 --- a/src/cleaners/skyblock/member.ts +++ b/src/cleaners/skyblock/member.ts @@ -30,7 +30,7 @@ export interface CleanMember extends CleanBasicMember { rawHypixelStats?: { [ key: string ]: number } minions: CleanMinion[] fairy_souls: FairySouls - inventories: Inventories + inventories?: Inventories objectives: Objective[] skills: Skill[] visited_zones: Zone[] @@ -38,8 +38,9 @@ export interface CleanMember extends CleanBasicMember { slayers: SlayerData } -export async function cleanSkyBlockProfileMemberResponseBasic(member: any, included: Included[] = null): Promise<CleanBasicMember> { +export async function cleanSkyBlockProfileMemberResponseBasic(member: any): Promise<CleanBasicMember | null> { const player = await cached.fetchPlayer(member.uuid) + if (!player) return null return { uuid: member.uuid, username: player.username, @@ -50,11 +51,11 @@ export async function cleanSkyBlockProfileMemberResponseBasic(member: any, inclu } /** Cleans up a member (from skyblock/profile) */ -export async function cleanSkyBlockProfileMemberResponse(member, included: Included[] = null): Promise<CleanMember> { +export async function cleanSkyBlockProfileMemberResponse(member, included: Included[] | undefined = undefined): Promise<CleanMember | null> { // profiles.members[] - const inventoriesIncluded = included === null || included.includes('inventories') + const inventoriesIncluded = included === undefined || included.includes('inventories') const player = await cached.fetchPlayer(member.uuid) - if (!player) return + if (!player) return null const fairySouls = cleanFairySouls(member) const { max_fairy_souls: maxFairySouls } = await constants.fetchConstantValues() @@ -109,5 +110,5 @@ export interface CleanMemberProfilePlayer extends CleanPlayer { export interface CleanMemberProfile { member: CleanMemberProfilePlayer profile: CleanFullProfileBasicMembers - customization: AccountCustomization + customization?: AccountCustomization } diff --git a/src/cleaners/skyblock/minions.ts b/src/cleaners/skyblock/minions.ts index 21f7b66..d045a7e 100644 --- a/src/cleaners/skyblock/minions.ts +++ b/src/cleaners/skyblock/minions.ts @@ -80,7 +80,7 @@ export function combineMinionArrays(minions: CleanMinion[][]): CleanMinion[] { // This should never happen, but in case the length of `minion.levels` is longer than // `matchingMinionReference.levels`, then it should be extended to be equal length while (matchingMinionReference.levels.length < minion.levels.length) - matchingMinionReference.levels.push(null) + matchingMinionReference.levels.push(false) for (let i = 0; i < minion.levels.length; i++) { if (minion.levels[i]) diff --git a/src/cleaners/skyblock/profile.ts b/src/cleaners/skyblock/profile.ts index 0453ff3..5bcea4b 100644 --- a/src/cleaners/skyblock/profile.ts +++ b/src/cleaners/skyblock/profile.ts @@ -25,7 +25,7 @@ export interface CleanFullProfileBasicMembers extends CleanProfile { /** Return a `CleanProfile` instead of a `CleanFullProfile`, useful when we need to get members but don't want to waste much ram */ export async function cleanSkyblockProfileResponseLighter(data): Promise<CleanProfile> { // We use Promise.all so it can fetch all the usernames at once instead of waiting for the previous promise to complete - const promises: Promise<CleanBasicMember>[] = [] + const promises: Promise<CleanBasicMember | null>[] = [] for (const memberUUID in data.members) { const memberRaw = data.members[memberUUID] @@ -34,7 +34,7 @@ export async function cleanSkyblockProfileResponseLighter(data): Promise<CleanPr promises.push(cleanSkyBlockProfileMemberResponseBasic(memberRaw)) } - const cleanedMembers: CleanBasicMember[] = await Promise.all(promises) + const cleanedMembers: CleanBasicMember[] = (await Promise.all(promises)).filter(m => m) as CleanBasicMember[] return { uuid: data.profile_id, @@ -48,7 +48,7 @@ export async function cleanSkyblockProfileResponseLighter(data): Promise<CleanPr */ export async function cleanSkyblockProfileResponse(data: any, options?: ApiOptions): Promise<CleanFullProfile|CleanProfile> { // 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>[] = [] + const promises: Promise<CleanMember | null>[] = [] for (const memberUUID in data.members) { const memberRaw = data.members[memberUUID] @@ -62,7 +62,7 @@ export async function cleanSkyblockProfileResponse(data: any, options?: ApiOptio )) } - const cleanedMembers: CleanMember[] = (await Promise.all(promises)).filter(m => m !== null && m !== undefined) + const cleanedMembers: CleanMember[] = (await Promise.all(promises)).filter(m => m !== null && m !== undefined) as CleanMember[] if (options?.basic) { return { diff --git a/src/cleaners/skyblock/profiles.ts b/src/cleaners/skyblock/profiles.ts index f12b5f6..a44fe48 100644 --- a/src/cleaners/skyblock/profiles.ts +++ b/src/cleaners/skyblock/profiles.ts @@ -1,5 +1,10 @@ -import { HypixelPlayerStatsSkyBlockProfiles } from "../../hypixelApi" -import { CleanBasicProfile, CleanProfile, cleanSkyblockProfileResponse, cleanSkyblockProfileResponseLighter } from "./profile" +import { HypixelPlayerStatsSkyBlockProfiles } from '../../hypixelApi' +import { + CleanBasicProfile, + CleanFullProfile, + CleanProfile, + cleanSkyblockProfileResponse +} from './profile' export function cleanPlayerSkyblockProfiles(rawProfiles: HypixelPlayerStatsSkyBlockProfiles): CleanBasicProfile[] { let profiles: CleanBasicProfile[] = [] @@ -14,7 +19,7 @@ export function cleanPlayerSkyblockProfiles(rawProfiles: HypixelPlayerStatsSkyBl /** Convert an array of raw profiles into clean profiles */ export async function cleanSkyblockProfilesResponse(data: any[]): Promise<CleanProfile[]> { - const promises = [] + const promises: Promise<CleanProfile | CleanFullProfile>[] = [] for (const profile of data ?? []) { // let cleanedProfile = await cleanSkyblockProfileResponseLighter(profile) promises.push(cleanSkyblockProfileResponse(profile)) diff --git a/src/cleaners/skyblock/stats.ts b/src/cleaners/skyblock/stats.ts index 31a894c..a91ca74 100644 --- a/src/cleaners/skyblock/stats.ts +++ b/src/cleaners/skyblock/stats.ts @@ -14,8 +14,8 @@ const statCategories: { [ key: string ]: string[] | null } = { // sorted in orde } export interface StatCategory { - category: string, - name: string + category: string | null + name: string | null } export function categorizeStat(statNameRaw: string): StatCategory { @@ -71,11 +71,11 @@ export interface StatItem { rawName: string value: number categorizedName: string - category: string - unit: string + category: string | null + unit: string | null } -export function getStatUnit(name: string): string { +export function getStatUnit(name: string): string | null { for (const [ unitName, statMatchers ] of Object.entries(statUnits)) { for (const statMatch of statMatchers) { let trailingEnd = statMatch[0] === '_' @@ -88,6 +88,7 @@ export function getStatUnit(name: string): string { return unitName } } + return null } diff --git a/src/constants.ts b/src/constants.ts index 9699e57..dcb9acf 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -40,7 +40,7 @@ async function fetchGithubApi(method: string, route: string, headers?: any, json githubApiBase + route, { agent: () => httpsAgent, - body: json ? JSON.stringify(json) : null, + body: json ? JSON.stringify(json) : undefined, method, headers: Object.assign({ 'Authorization': `token ${process.env.github_token}` @@ -78,7 +78,7 @@ function fetchFile(path: string): Promise<GithubFile> { return new Promise(resolve => { queue.enqueue(async() => { if (fileCache.has(path)) - return resolve(fileCache.get(path)) + return resolve(fileCache.get(path)!) const r = await fetchGithubApi( 'GET', diff --git a/src/database.ts b/src/database.ts index 60fbc7e..5d15954 100644 --- a/src/database.ts +++ b/src/database.ts @@ -59,7 +59,7 @@ interface profileRawLeaderboardItem { } interface MemberLeaderboardItem { - player: CleanPlayer + player: CleanPlayer | null profileUuid: string value: number } @@ -205,9 +205,11 @@ export async function fetchAllLeaderboardsCategorized(): Promise<{ [ category: s const categorizedLeaderboards: { [ category: string ]: string[] } = {} for (const leaderboard of [...memberLeaderboardAttributes, ...profileLeaderboardAttributes]) { const { category } = categorizeStat(leaderboard) - if (!categorizedLeaderboards[category]) - categorizedLeaderboards[category] = [] - categorizedLeaderboards[category].push(leaderboard) + if (category) { + if (!categorizedLeaderboards[category]) + categorizedLeaderboards[category] = [] + categorizedLeaderboards[category].push(leaderboard) + } } // move misc to end by removing and readding it @@ -366,14 +368,14 @@ async function fetchProfileLeaderboardRaw(name: string): Promise<profileRawLeade interface MemberLeaderboard { name: string - unit?: string + unit: string | null list: MemberLeaderboardItem[] info?: string } interface ProfileLeaderboard { name: string - unit?: string + unit: string | null list: ProfileLeaderboardItem[] info?: string } @@ -384,8 +386,9 @@ export async function fetchMemberLeaderboard(name: string): Promise<MemberLeader const leaderboardRaw = await fetchMemberLeaderboardRaw(name) const fetchLeaderboardPlayer = async(i: memberRawLeaderboardItem): Promise<MemberLeaderboardItem> => { + const player = await cached.fetchBasicPlayer(i.uuid) return { - player: await cached.fetchBasicPlayer(i.uuid), + player, profileUuid: i.profile, value: i.value } @@ -408,9 +411,12 @@ export async function fetchProfileLeaderboard(name: string): Promise<ProfileLead const leaderboardRaw = await fetchProfileLeaderboardRaw(name) const fetchLeaderboardProfile = async(i: profileRawLeaderboardItem): Promise<ProfileLeaderboardItem> => { - const players = [] - for (const playerUuid of i.players) - players.push(await cached.fetchBasicPlayer(playerUuid)) + const players: CleanPlayer[] = [] + for (const playerUuid of i.players) { + const player = await cached.fetchBasicPlayer(playerUuid) + if (player) + players.push(player) + } return { players: players, profileUuid: i.uuid, @@ -445,17 +451,26 @@ export async function fetchLeaderboard(name: string): Promise<MemberLeaderboard| return leaderboard } +interface LeaderboardSpot { + name: string + positionIndex: number + value: number + unit: string | null +} + /** Get the leaderboard positions a member is on. This may take a while depending on whether stuff is cached */ -export async function fetchMemberLeaderboardSpots(player: string, profile: string) { +export async function fetchMemberLeaderboardSpots(player: string, profile: string): Promise<LeaderboardSpot[] | null> { const fullProfile = await cached.fetchProfile(player, profile) + if (!fullProfile) return null const fullMember = fullProfile.members.find(m => m.username.toLowerCase() === player.toLowerCase() || m.uuid === player) + if (!fullMember) return null // update the leaderboard positions for the member await updateDatabaseMember(fullMember, fullProfile) const applicableAttributes = await getApplicableMemberLeaderboardAttributes(fullMember) - const memberLeaderboardSpots = [] + const memberLeaderboardSpots: LeaderboardSpot[] = [] for (const leaderboardName in applicableAttributes) { const leaderboard = await fetchMemberLeaderboardRaw(leaderboardName) @@ -472,7 +487,7 @@ export async function fetchMemberLeaderboardSpots(player: string, profile: strin return memberLeaderboardSpots } -async function getLeaderboardRequirement(name: string, leaderboardType: 'member' | 'profile'): Promise<number> { +async function getLeaderboardRequirement(name: string, leaderboardType: 'member' | 'profile'): Promise<number | null> { let leaderboard: memberRawLeaderboardItem[] | profileRawLeaderboardItem[] if (leaderboardType === 'member') leaderboard = await fetchMemberLeaderboardRaw(name) @@ -480,8 +495,8 @@ async function getLeaderboardRequirement(name: string, leaderboardType: 'member' leaderboard = await fetchProfileLeaderboardRaw(name) // if there's more than 100 items, return the 100th. if there's less, return null - if (leaderboard.length >= leaderboardMax) - return leaderboard[leaderboardMax - 1].value + if (leaderboard!.length >= leaderboardMax) + return leaderboard![leaderboardMax - 1].value else return null } @@ -504,7 +519,7 @@ async function getApplicableMemberLeaderboardAttributes(member: CleanMember): Pr let leaderboardsCount: number = Object.keys(applicableAttributes).length - const leaderboardsCountRequirement: number = await getLeaderboardRequirement('leaderboards_count', 'member') + const leaderboardsCountRequirement: number | null = await getLeaderboardRequirement('leaderboards_count', 'member') if ( (leaderboardsCountRequirement === null) @@ -534,11 +549,11 @@ async function getApplicableProfileLeaderboardAttributes(profile: CleanFullProfi let leaderboardsCount: number = Object.keys(applicableAttributes).length - const leaderboardsCountRequirement: number = await getLeaderboardRequirement('leaderboards_count', 'member') + const leaderboardsCountRequirement: number | null = await getLeaderboardRequirement('leaderboards_count', 'member') if ( - (leaderboardsCountRequirement === null) - || (leaderboardsCount > leaderboardsCountRequirement) + leaderboardsCountRequirement === null + || leaderboardsCount > leaderboardsCountRequirement ) { applicableAttributes['leaderboards_count'] = leaderboardsCount } @@ -557,7 +572,8 @@ export async function updateDatabaseMember(member: CleanMember, profile: CleanFu if (debug) console.debug('adding member to leaderboards', member.username) - await constants.addStats(Object.keys(member.rawHypixelStats)) + if (member.rawHypixelStats) + await constants.addStats(Object.keys(member.rawHypixelStats)) await constants.addCollections(member.collections.map(coll => coll.name)) await constants.addSkills(member.skills.map(skill => skill.name)) await constants.addZones(member.visited_zones.map(zone => zone.name)) @@ -738,15 +754,15 @@ export async function createSession(refreshToken: string, userData: discord.Disc return sessionId } -export async function fetchSession(sessionId: string): Promise<SessionSchema> { +export async function fetchSession(sessionId: string): Promise<SessionSchema | null> { return await sessionsCollection?.findOne({ _id: sessionId }) } -export async function fetchAccount(minecraftUuid: string): Promise<AccountSchema> { +export async function fetchAccount(minecraftUuid: string): Promise<AccountSchema | null> { return await accountsCollection?.findOne({ minecraftUuid }) } -export async function fetchAccountFromDiscord(discordId: string): Promise<AccountSchema> { +export async function fetchAccountFromDiscord(discordId: string): Promise<AccountSchema | null> { return await accountsCollection?.findOne({ discordId }) } diff --git a/src/discord.ts b/src/discord.ts index 48146b5..fac5438 100644 --- a/src/discord.ts +++ b/src/discord.ts @@ -26,9 +26,13 @@ export interface DiscordUser { mfa_enabled: boolean } -export async function exchangeCode(redirectUri: string, code: string): Promise<TokenResponse> { +export async function exchangeCode(redirectUri: string, code: string): Promise<TokenResponse | null> { const API_ENDPOINT = 'https://discord.com/api/v6' const CLIENT_SECRET = process.env.discord_client_secret + if (!CLIENT_SECRET) { + console.error('discord_client_secret isn\'t in env, couldn\'t login with discord') + return null + } const data = { 'client_id': DISCORD_CLIENT_ID, 'client_secret': CLIENT_SECRET, diff --git a/src/hypixel.ts b/src/hypixel.ts index eb66d48..d6f9661 100644 --- a/src/hypixel.ts +++ b/src/hypixel.ts @@ -11,7 +11,7 @@ import { CleanPlayer, cleanPlayerResponse } from './cleaners/player' import * as cached from './hypixelCached' import { debug } from '.' -export type Included = 'profiles' | 'player' | 'stats' | 'inventories' +export type Included = 'profiles' | 'player' | 'stats' | 'inventories' | undefined // the interval at which the "last_save" parameter updates in the hypixel api, this is 3 minutes export const saveInterval = 60 * 3 * 1000 @@ -62,7 +62,7 @@ export interface UserAny { } export interface CleanUser { - player: CleanPlayer + player: CleanPlayer | null profiles?: CleanProfile[] activeProfile?: string online?: boolean @@ -76,24 +76,25 @@ export interface CleanUser { * @param included lets you choose what is returned, so there's less processing required on the backend * used inclusions: player, profiles */ -export async function fetchUser({ user, uuid, username }: UserAny, included: Included[]=['player'], customization?: boolean): Promise<CleanUser> { +export async function fetchUser({ user, uuid, username }: UserAny, included: Included[]=['player'], customization?: boolean): Promise<CleanUser | null> { if (!uuid) { // If the uuid isn't provided, get it - uuid = await cached.uuidFromUser(user || username) + if (!username && !user) return null + uuid = await cached.uuidFromUser((user ?? username)!) } - const websiteAccountPromise = customization ? fetchAccount(uuid) : null if (!uuid) { // the user doesn't exist. if (debug) console.debug('error:', user, 'doesnt exist') return null } + const websiteAccountPromise = customization ? fetchAccount(uuid) : null const includePlayers = included.includes('player') const includeProfiles = included.includes('profiles') - let profilesData: CleanProfile[] - let basicProfilesData: CleanBasicProfile[] - let playerData: CleanPlayer + let profilesData: CleanProfile[] | undefined + let basicProfilesData: CleanBasicProfile[] | undefined + let playerData: CleanPlayer | null = null if (includePlayers) { playerData = await cached.fetchPlayer(uuid) @@ -103,31 +104,30 @@ export async function fetchUser({ user, uuid, username }: UserAny, included: Inc if (playerData) delete playerData.profiles } - if (includeProfiles) { + if (includeProfiles) profilesData = await cached.fetchSkyblockProfiles(uuid) - } - let activeProfile: CleanProfile = null + let activeProfile: CleanProfile 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) { + for (const profile of profilesData!) { + const member = profile.members?.find(member => member.uuid === uuid) + if (member && member.last_save > lastOnline) { lastOnline = member.last_save activeProfile = profile } } } - let websiteAccount: AccountSchema = undefined + let websiteAccount: AccountSchema | null = null - if (websiteAccountPromise) { + if (websiteAccountPromise) websiteAccount = await websiteAccountPromise - } + return { - player: playerData ?? null, + player: playerData, profiles: profilesData ?? basicProfilesData, - activeProfile: includeProfiles ? activeProfile?.uuid : undefined, + activeProfile: includeProfiles ? activeProfile!?.uuid : undefined, online: includeProfiles ? lastOnline > (Date.now() - saveInterval): undefined, customization: websiteAccount?.customization } @@ -140,9 +140,9 @@ export async function fetchUser({ user, uuid, username }: UserAny, included: Inc * @param profile A profile name or profile uuid * @param customization Whether stuff like the user's custom background will be returned */ -export async function fetchMemberProfile(user: string, profile: string, customization: boolean): Promise<CleanMemberProfile> { +export async function fetchMemberProfile(user: string, profile: string, customization: boolean): Promise<CleanMemberProfile | null> { const playerUuid = await cached.uuidFromUser(user) - if (!playerUuid) return + if (!playerUuid) return null // we don't await the promise immediately so it can load while we do other stuff const websiteAccountPromise = customization ? fetchAccount(playerUuid) : null const profileUuid = await cached.fetchProfileUuid(user, profile) @@ -153,9 +153,12 @@ export async function fetchMemberProfile(user: string, profile: string, customiz const player = await cached.fetchPlayer(playerUuid) + if (!player) return null // this should never happen, but if it does just return null + const cleanProfile = await cached.fetchProfile(playerUuid, profileUuid) as CleanFullProfileBasicMembers const member = cleanProfile.members.find(m => m.uuid === playerUuid) + if (!member) return null // this should never happen, but if it does just return null // remove unnecessary member data const simpleMembers: CleanBasicMember[] = cleanProfile.members.map(m => { @@ -170,7 +173,7 @@ export async function fetchMemberProfile(user: string, profile: string, customiz cleanProfile.members = simpleMembers - let websiteAccount: AccountSchema = undefined + let websiteAccount: AccountSchema | null = null if (websiteAccountPromise) websiteAccount = await websiteAccountPromise @@ -178,7 +181,7 @@ export async function fetchMemberProfile(user: string, profile: string, customiz 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 @@ -200,7 +203,7 @@ export async function fetchMemberProfile(user: string, profile: string, customiz path: 'skyblock/profile', args: { profile: profileUuid } }, - null, + undefined, { mainMemberUuid: playerUuid } ) @@ -223,7 +226,7 @@ export async function fetchMemberProfile(user: string, profile: string, customiz path: 'skyblock/profile', args: { profile: profileUuid } }, - null, + undefined, { basic: true } ) @@ -237,7 +240,7 @@ export async function fetchMemberProfilesUncached(playerUuid: string): Promise<C args: { uuid: playerUuid }}, - null, + undefined, { // only the inventories for the main player are generated, this is for optimization purposes mainMemberUuid: playerUuid diff --git a/src/hypixelApi.ts b/src/hypixelApi.ts index b3a1cae..1b57d8f 100644 --- a/src/hypixelApi.ts +++ b/src/hypixelApi.ts @@ -32,10 +32,10 @@ const baseHypixelAPI = 'https://api.hypixel.net' /** Choose the best current API key */ -export function chooseApiKey(): string { +export function chooseApiKey(): string | null { // find the api key with the lowest amount of uses - let bestKeyUsage: KeyUsage = null - let bestKey: string = null + let bestKeyUsage: KeyUsage | null = null + let bestKey: string | null = null for (let key of shuffle(apiKeys.slice())) { const keyUsage = apiKeyUsage[key] @@ -173,9 +173,9 @@ export async function sendApiRequest({ path, key, args }): Promise<HypixelRespon if (fetchResponse.headers.get('ratelimit-limit')) // remember how many uses it has apiKeyUsage[key] = { - remaining: parseInt(fetchResponse.headers.get('ratelimit-remaining')), - limit: parseInt(fetchResponse.headers.get('ratelimit-limit')), - reset: Date.now() + parseInt(fetchResponse.headers.get('ratelimit-reset')) * 1000 + remaining: parseInt(fetchResponse.headers.get('ratelimit-remaining') ?? '0'), + limit: parseInt(fetchResponse.headers.get('ratelimit-limit') ?? '0'), + reset: Date.now() + parseInt(fetchResponse.headers.get('ratelimit-reset') ?? '0') * 1000 } if (fetchJsonParsed.throttle) { diff --git a/src/hypixelCached.ts b/src/hypixelCached.ts index 92a7801..4aa96ef 100644 --- a/src/hypixelCached.ts +++ b/src/hypixelCached.ts @@ -81,17 +81,17 @@ function waitForCacheSet(cache: NodeCache, key?: string, value?: string): Promis * 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> { +export async function uuidFromUser(user: string): Promise<string | undefined> { // if the user is 32 characters long, it has to be a uuid if (isUuid(user)) return undashUuid(user) if (usernameCache.has(undashUuid(user))) { // check if the uuid is a key - const username: Promise<KeyValue> | string | null = usernameCache.get<string | Promise<KeyValue>>(undashUuid(user)) + const username: Promise<KeyValue> | string | undefined = usernameCache.get<string | Promise<KeyValue>>(undashUuid(user)) // sometimes the username will be null, return that - if (username === null) return null + if (username === null) return undefined // if it has .then, then that means its a waitForCacheSet promise. This is done to prevent requests made while it is already requesting if ((username as Promise<KeyValue>).then) { @@ -136,15 +136,16 @@ export async function uuidFromUser(user: string): Promise<string> { * Fetch the username from a user * @param user A user can be either a uuid or a username */ -export async function usernameFromUser(user: string): Promise<string> { +export async function usernameFromUser(user: string): Promise<string | null> { if (usernameCache.has(undashUuid(user))) { if (debug) console.debug('Cache hit! usernameFromUser', user) - return usernameCache.get(undashUuid(user)) + return usernameCache.get(undashUuid(user)) ?? null } if (debug) console.debug('Cache miss: usernameFromUser', user) let { uuid, username } = await mojang.profileFromUser(user) + if (!uuid) return null uuid = undashUuid(uuid) usernameCache.set(uuid, username) return username @@ -152,12 +153,12 @@ export async function usernameFromUser(user: string): Promise<string> { let fetchingPlayers: Set<string> = new Set() -export async function fetchPlayer(user: string): Promise<CleanPlayer> { +export async function fetchPlayer(user: string): Promise<CleanPlayer | null> { const playerUuid = await uuidFromUser(user) - + if (!playerUuid) return null if (playerCache.has(playerUuid)) - return playerCache.get(playerUuid) + return playerCache.get(playerUuid)! // if it's already in the process of fetching, check every 100ms until it's not fetching the player anymore and fetch it again, since it'll be cached now if (fetchingPlayers.has(playerUuid)) { @@ -176,7 +177,7 @@ export async function fetchPlayer(user: string): Promise<CleanPlayer> { fetchingPlayers.delete(playerUuid) - if (!cleanPlayer) return + if (!cleanPlayer) return null // clone in case it gets modified somehow later playerCache.set(playerUuid, cleanPlayer) @@ -190,14 +191,19 @@ export async function fetchPlayer(user: string): Promise<CleanPlayer> { } /** Fetch a player without their profiles. This is heavily cached. */ -export async function fetchBasicPlayer(user: string): Promise<CleanPlayer> { +export async function fetchBasicPlayer(user: string): Promise<CleanPlayer | null> { const playerUuid = await uuidFromUser(user) + if (!playerUuid) return null + if (basicPlayerCache.has(playerUuid)) - return basicPlayerCache.get(playerUuid) + return basicPlayerCache.get(playerUuid)! const player = await fetchPlayer(playerUuid) - if (!player) console.debug('no player? this should never happen', user, playerUuid) + if (!player) { + console.debug('no player? this should never happen', user, playerUuid) + return null + } delete player.profiles return player @@ -206,7 +212,7 @@ export async function fetchBasicPlayer(user: string): Promise<CleanPlayer> { export async function fetchSkyblockProfiles(playerUuid: string): Promise<CleanProfile[]> { if (profilesCache.has(playerUuid)) { if (debug) console.debug('Cache hit! fetchSkyblockProfiles', playerUuid) - return profilesCache.get(playerUuid) + return profilesCache.get(playerUuid)! } if (debug) console.debug('Cache miss: fetchSkyblockProfiles', playerUuid) @@ -220,7 +226,7 @@ export async function fetchSkyblockProfiles(playerUuid: string): Promise<CleanPr const basicProfile: CleanProfile = { name: profile.name, uuid: profile.uuid, - members: profile.members.map(m => { + members: profile.members?.map(m => { return { uuid: m.uuid, username: m.username, @@ -240,14 +246,14 @@ export async function fetchSkyblockProfiles(playerUuid: string): Promise<CleanPr } /** Fetch an array of `BasicProfile`s */ -async function fetchBasicProfiles(user: string): Promise<CleanBasicProfile[]> { +async function fetchBasicProfiles(user: string): Promise<CleanBasicProfile[] | null> { const playerUuid = await uuidFromUser(user) - if (!playerUuid) return // invalid player, just return + if (!playerUuid) return null // invalid player, just return if (basicProfilesCache.has(playerUuid)) { if (debug) console.debug('Cache hit! fetchBasicProfiles', playerUuid) - return basicProfilesCache.get(playerUuid) + return basicProfilesCache.get(playerUuid)! } if (debug) console.debug('Cache miss: fetchBasicProfiles', user) @@ -259,6 +265,7 @@ async function fetchBasicProfiles(user: string): Promise<CleanBasicProfile[]> { } const profiles = player.profiles basicProfilesCache.set(playerUuid, profiles) + if (!profiles) return null // cache the profile names and uuids to profileNameCache because we can for (const profile of profiles) @@ -272,7 +279,7 @@ async function fetchBasicProfiles(user: string): Promise<CleanBasicProfile[]> { * @param user A username or uuid * @param profile A profile name or profile uuid */ -export async function fetchProfileUuid(user: string, profile: string): Promise<string> { +export async function fetchProfileUuid(user: string, profile: string): Promise<string | null> { // if a profile wasn't provided, return if (!profile) { if (debug) console.debug('no profile provided?', user, profile) @@ -282,12 +289,12 @@ export async function fetchProfileUuid(user: string, profile: string): Promise<s if (debug) console.debug('Cache miss: fetchProfileUuid', user, profile) const profiles = await fetchBasicProfiles(user) - if (!profiles) return // user probably doesnt exist + if (!profiles) return null // user probably doesnt exist const profileUuid = undashUuid(profile) for (const p of profiles) { - if (p.name.toLowerCase() === profileUuid.toLowerCase()) + if (p.name?.toLowerCase() === profileUuid.toLowerCase()) return undashUuid(p.uuid) else if (undashUuid(p.uuid) === undashUuid(profileUuid)) return undashUuid(p.uuid) @@ -300,8 +307,9 @@ export async function fetchProfileUuid(user: string, profile: string): Promise<s * @param user A username or uuid * @param profile A profile name or profile uuid */ -export async function fetchProfile(user: string, profile: string): Promise<CleanFullProfile> { +export async function fetchProfile(user: string, profile: string): Promise<CleanFullProfile | null> { const playerUuid = await uuidFromUser(user) + if (!playerUuid) return null const profileUuid = await fetchProfileUuid(playerUuid, profile) if (!profileUuid) return null @@ -309,13 +317,15 @@ export async function fetchProfile(user: string, profile: string): Promise<Clean if (profileCache.has(profileUuid)) { // we have the profile cached, return it :) if (debug) console.debug('Cache hit! fetchProfile', profileUuid) - return profileCache.get(profileUuid) + return profileCache.get(profileUuid)! } if (debug) console.debug('Cache miss: fetchProfile', user, profile) const profileName = await fetchProfileName(user, profile) + if (!profileName) return null // uhh this should never happen but if it does just return null + const cleanProfile: CleanFullProfile = await hypixel.fetchMemberProfileUncached(playerUuid, profileUuid) // we know the name from fetchProfileName, so set it here @@ -330,11 +340,12 @@ export async function fetchProfile(user: string, profile: string): Promise<Clean * Fetch a CleanProfile from the uuid * @param profileUuid A profile name or profile uuid */ -export async function fetchBasicProfileFromUuid(profileUuid: string): Promise<CleanProfile> { +export async function fetchBasicProfileFromUuid(profileUuid: string): Promise<CleanProfile | undefined> { if (profileCache.has(profileUuid)) { // we have the profile cached, return it :) if (debug) console.debug('Cache hit! fetchBasicProfileFromUuid', profileUuid) - const profile: CleanFullProfile = profileCache.get(profileUuid) + const profile: CleanFullProfile | undefined = profileCache.get(profileUuid) + if (!profile) return undefined return { uuid: profile.uuid, members: profile.members.map(m => ({ @@ -357,26 +368,32 @@ export async function fetchBasicProfileFromUuid(profileUuid: string): Promise<Cl * @param user A player uuid or username * @param profile A profile uuid or name */ -export async function fetchProfileName(user: string, profile: string): Promise<string> { +export async function fetchProfileName(user: string, profile: string): Promise<string | null> { // we're fetching the profile and player uuid again in case we were given a name, but it's cached so it's not much of a problem const profileUuid = await fetchProfileUuid(user, profile) - if (!profileUuid) - return null + if (!profileUuid) return null + const playerUuid = await uuidFromUser(user) + if (!playerUuid) return null + if (profileNameCache.has(`${playerUuid}.${profileUuid}`)) { // Return the profile name if it's cached if (debug) console.debug('Cache hit! fetchProfileName', profileUuid) - return profileNameCache.get(`${playerUuid}.${profileUuid}`) + return profileNameCache.get!(`${playerUuid}.${profileUuid}`) ?? null } if (debug) console.debug('Cache miss: fetchProfileName', user, profile) const basicProfiles = await fetchBasicProfiles(playerUuid) - let profileName + + if (!basicProfiles) return null + + let profileName: string | null = null + for (const basicProfile of basicProfiles) if (basicProfile.uuid === playerUuid) - profileName = basicProfile.name + profileName = basicProfile.name ?? null profileNameCache.set(`${playerUuid}.${profileUuid}`, profileName) return profileName diff --git a/src/index.ts b/src/index.ts index 4f4ad00..647038f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -132,7 +132,12 @@ app.get('/constants', async(req, res) => { app.post('/accounts/createsession', async(req, res) => { try { const { code } = req.body - const { access_token: accessToken, refresh_token: refreshToken } = await discord.exchangeCode(`${mainSiteUrl}/loggedin`, code) + const codeExchange = await discord.exchangeCode(`${mainSiteUrl}/loggedin`, code) + if (!codeExchange) { + res.json({ ok: false, error: 'discord_client_secret isn\'t in env' }) + return + } + const { access_token: accessToken, refresh_token: refreshToken } = codeExchange if (!accessToken) // access token is invalid :( return res.json({ ok: false }) @@ -148,6 +153,8 @@ app.post('/accounts/session', async(req, res) => { try { const { uuid } = req.body const session = await fetchSession(uuid) + if (!session) + return res.json({ ok: false }) const account = await fetchAccountFromDiscord(session.discord_user.id) res.json({ session, account }) } catch (err) { diff --git a/src/mojang.ts b/src/mojang.ts index e22c8d8..84c8c7d 100644 --- a/src/mojang.ts +++ b/src/mojang.ts @@ -14,9 +14,8 @@ const httpsAgent = new Agent({ interface MojangApiResponse { /** These uuids are already undashed */ - uuid: string - - username: string + uuid: string | null + username: string | null } /** @@ -73,7 +72,7 @@ export async function profileFromUsername(username: string): Promise<MojangApiRe return await profileFromUsername(username) } - let data = null + let data: any = null const rawData = await fetchResponse.text() try { data = JSON.parse(rawData) diff --git a/src/util.ts b/src/util.ts index 67cf4b5..45839cc 100644 --- a/src/util.ts +++ b/src/util.ts @@ -61,7 +61,7 @@ export const minecraftColorCodes: { [ key: string ]: string } = { * For example: blue -> 9 * @param colorName The name of the color (blue, red, aqua, etc) */ -export function colorCodeFromName(colorName: string): string { +export function colorCodeFromName(colorName: string): string | undefined { const hexColor = minecraftColorCodes[colorName.toLowerCase()] for (const key in minecraftColorCodes) { const value = minecraftColorCodes[key] diff --git a/tsconfig.json b/tsconfig.json index 43f0b28..d54082d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ "lib": ["esnext", "dom", "DOM.Iterable"], "target": "es2019", "esModuleInterop": true, - "outDir": "build" + "outDir": "build", + "strictNullChecks": true }, "include": ["src"], "exclude": ["**/node_modules"], |