diff options
Diffstat (limited to 'src/database.ts')
-rw-r--r-- | src/database.ts | 248 |
1 files changed, 222 insertions, 26 deletions
diff --git a/src/database.ts b/src/database.ts index 983dcb2..c715d52 100644 --- a/src/database.ts +++ b/src/database.ts @@ -3,7 +3,7 @@ */ import { categorizeStat, getStatUnit } from './cleaners/skyblock/stats' -import { CleanFullProfile, CleanProfile } from './cleaners/skyblock/profile' +import { CleanBasicProfile, CleanFullProfile, CleanProfile } from './cleaners/skyblock/profile' import { CleanMember } from './cleaners/skyblock/member' import { Collection, Db, MongoClient } from 'mongodb' import { CleanPlayer } from './cleaners/player' @@ -22,19 +22,32 @@ const recentlyUpdated = new NodeCache({ useClones: false, }) -interface DatabaseLeaderboardItem { +interface DatabaseMemberLeaderboardItem { uuid: string profile: string stats: any last_updated: Date } +interface DatabaseProfileLeaderboardItem { + uuid: string + /** An array of uuids for each player in the profile */ + players: string[] + stats: any + last_updated: Date +} -interface LeaderboardItem { +interface MemberLeaderboardItem { player: CleanPlayer + profileUuid: string + value: number +} +interface ProfileLeaderboardItem { + players: CleanPlayer[] + profileUuid: string value: number } -const cachedRawLeaderboards: Map<string, DatabaseLeaderboardItem[]> = new Map() +const cachedRawLeaderboards: Map<string, (DatabaseMemberLeaderboardItem|DatabaseProfileLeaderboardItem)[]> = new Map() const leaderboardMax = 100 const reversedLeaderboards = [ @@ -45,6 +58,7 @@ const reversedLeaderboards = [ let client: MongoClient let database: Db let memberLeaderboardsCollection: Collection<any> +let profileLeaderboardsCollection: Collection<any> async function connect(): Promise<void> { if (!process.env.db_uri) @@ -54,6 +68,7 @@ async function connect(): Promise<void> { client = await MongoClient.connect(process.env.db_uri, { useNewUrlParser: true, useUnifiedTopology: true }) database = client.db(process.env.db_name) memberLeaderboardsCollection = database.collection('member-leaderboards') + profileLeaderboardsCollection = database.collection('profile-leaderboards') } interface StringNumber { @@ -117,11 +132,20 @@ function getMemberLeaderboardAttributes(member: CleanMember): StringNumber { } } +function getProfileLeaderboardAttributes(profile: CleanFullProfile): StringNumber { + // if you want to add a new leaderboard for member attributes, add it here (and getAllLeaderboardAttributes) + return { + minion_count: profile.minion_count + } +} + export async function fetchAllLeaderboardsCategorized(): Promise<{ [ category: string ]: string[] }> { const memberLeaderboardAttributes: string[] = await fetchAllMemberLeaderboardAttributes() + const profileLeaderboardAttributes: string[] = await fetchAllProfileLeaderboardAttributes() + const categorizedLeaderboards: { [ category: string ]: string[] } = {} - for (const leaderboard of memberLeaderboardAttributes) { + for (const leaderboard of [...memberLeaderboardAttributes, ...profileLeaderboardAttributes]) { const { category } = categorizeStat(leaderboard) if (!categorizedLeaderboards[category]) categorizedLeaderboards[category] = [] @@ -156,7 +180,7 @@ export async function fetchSlayerLeaderboards(): Promise<string[]> { return leaderboardNames } -/** Fetch the names of all the leaderboards */ +/** Fetch the names of all the leaderboards that rank members */ export 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 @@ -179,6 +203,13 @@ export async function fetchAllMemberLeaderboardAttributes(): Promise<string[]> { ] } +/** Fetch the names of all the leaderboards that rank profiles */ +async function fetchAllProfileLeaderboardAttributes(): Promise<string[]> { + return [ + 'minion_count' + ] +} + function isLeaderboardReversed(name: string): boolean { for (const leaderboardMatch of reversedLeaderboards) { let trailingEnd = leaderboardMatch[0] === '_' @@ -193,9 +224,11 @@ function isLeaderboardReversed(name: string): boolean { return false } -async function fetchMemberLeaderboardRaw(name: string): Promise<DatabaseLeaderboardItem[]> { +async function fetchMemberLeaderboardRaw(name: string): Promise<DatabaseMemberLeaderboardItem[]> { + if (!client) throw Error('Client isn\'t initialized yet') + if (cachedRawLeaderboards.has(name)) - return cachedRawLeaderboards.get(name) + return cachedRawLeaderboards.get(name) as DatabaseMemberLeaderboardItem[] // 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, '$ne': NaN } @@ -203,7 +236,7 @@ async function fetchMemberLeaderboardRaw(name: string): Promise<DatabaseLeaderbo const sortQuery: any = {} sortQuery[`stats.${name}`] = isLeaderboardReversed(name) ? 1 : -1 - const leaderboardRaw: DatabaseLeaderboardItem[] = await memberLeaderboardsCollection + const leaderboardRaw: DatabaseMemberLeaderboardItem[] = await memberLeaderboardsCollection .find(query) .sort(sortQuery) .limit(leaderboardMax) @@ -213,23 +246,52 @@ async function fetchMemberLeaderboardRaw(name: string): Promise<DatabaseLeaderbo return leaderboardRaw } -interface Leaderboard { +async function fetchProfileLeaderboardRaw(name: string): Promise<DatabaseProfileLeaderboardItem[]> { + if (cachedRawLeaderboards.has(name)) + return cachedRawLeaderboards.get(name) as DatabaseProfileLeaderboardItem[] + // 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, '$ne': NaN } + + const sortQuery: any = {} + sortQuery[`stats.${name}`] = isLeaderboardReversed(name) ? 1 : -1 + + const leaderboardRaw: DatabaseProfileLeaderboardItem[] = await profileLeaderboardsCollection + .find(query) + .sort(sortQuery) + .limit(leaderboardMax) + .toArray() + console.log('leaderboardRaw', leaderboardRaw) + + cachedRawLeaderboards.set(name, leaderboardRaw) + return leaderboardRaw +} + +interface MemberLeaderboard { + name: string + unit?: string + list: MemberLeaderboardItem[] +} + +interface ProfileLeaderboard { name: string unit?: string - list: LeaderboardItem[] + list: ProfileLeaderboardItem[] } + /** Fetch a leaderboard that ranks members, as opposed to profiles */ -export async function fetchMemberLeaderboard(name: string): Promise<Leaderboard> { +export async function fetchMemberLeaderboard(name: string): Promise<MemberLeaderboard> { const leaderboardRaw = await fetchMemberLeaderboardRaw(name) - const fetchLeaderboardPlayer = async(item: DatabaseLeaderboardItem): Promise<LeaderboardItem> => { + const fetchLeaderboardPlayer = async(item: DatabaseMemberLeaderboardItem): Promise<MemberLeaderboardItem> => { return { player: await cached.fetchBasicPlayer(item.uuid), + profileUuid: item.profile, value: item.stats[name] } } - const promises: Promise<LeaderboardItem>[] = [] + const promises: Promise<MemberLeaderboardItem>[] = [] for (const item of leaderboardRaw) { promises.push(fetchLeaderboardPlayer(item)) } @@ -241,6 +303,44 @@ export async function fetchMemberLeaderboard(name: string): Promise<Leaderboard> } } + +/** Fetch a leaderboard that ranks profiles, as opposed to members */ +export async function fetchProfileLeaderboard(name: string): Promise<ProfileLeaderboard> { + const leaderboardRaw = await fetchProfileLeaderboardRaw(name) + + const fetchLeaderboardProfile = async(item: DatabaseProfileLeaderboardItem): Promise<ProfileLeaderboardItem> => { + const players = [] + for (const playerUuid of item.players) + players.push(await cached.fetchBasicPlayer(playerUuid)) + return { + players: players, + profileUuid: item.uuid, + value: item.stats[name] + } + } + const promises: Promise<ProfileLeaderboardItem>[] = [] + for (const item of leaderboardRaw) { + promises.push(fetchLeaderboardProfile(item)) + } + const leaderboard = await Promise.all(promises) + return { + name: name, + unit: getStatUnit(name) ?? null, + list: leaderboard + } +} + +/** Fetch a leaderboard */ +export async function fetchLeaderboard(name: string): Promise<MemberLeaderboard|ProfileLeaderboard> { + const profileLeaderboards = await fetchAllProfileLeaderboardAttributes() + console.log(name, profileLeaderboards, profileLeaderboards.includes(name)) + if (profileLeaderboards.includes(name)) { + return await fetchProfileLeaderboard(name) + } else { + return await fetchMemberLeaderboard(name) + } +} + /** 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) { const fullProfile = await cached.fetchProfile(player, profile) @@ -249,7 +349,7 @@ export async function fetchMemberLeaderboardSpots(player: string, profile: strin // update the leaderboard positions for the member await updateDatabaseMember(fullMember, fullProfile) - const applicableAttributes = await getApplicableAttributes(fullMember) + const applicableAttributes = await getApplicableMemberLeaderboardAttributes(fullMember) const memberLeaderboardSpots = [] @@ -268,8 +368,12 @@ export async function fetchMemberLeaderboardSpots(player: string, profile: strin return memberLeaderboardSpots } -async function getMemberLeaderboardRequirement(name: string): Promise<number> { - const leaderboard = await fetchMemberLeaderboardRaw(name) +async function getLeaderboardRequirement(name: string, leaderboardType: 'member' | 'profile'): Promise<number> { + let leaderboard: DatabaseMemberLeaderboardItem[] | DatabaseProfileLeaderboardItem[] + if (leaderboardType === 'member') + leaderboard = await fetchMemberLeaderboardRaw(name) + else if (leaderboardType === 'profile') + leaderboard = await fetchProfileLeaderboardRaw(name) // if there's more than 100 items, return the 100th. if there's less, return null if (leaderboard.length >= leaderboardMax) @@ -279,12 +383,12 @@ async function getMemberLeaderboardRequirement(name: string): Promise<number> { } /** Get the attributes for the member, but only ones that would put them on the top 100 for leaderboards */ -async function getApplicableAttributes(member: CleanMember): Promise<StringNumber> { +async function getApplicableMemberLeaderboardAttributes(member: CleanMember): Promise<StringNumber> { const leaderboardAttributes = getMemberLeaderboardAttributes(member) const applicableAttributes = {} for (const [ leaderboard, attributeValue ] of Object.entries(leaderboardAttributes)) { - const requirement = await getMemberLeaderboardRequirement(leaderboard) + const requirement = await getLeaderboardRequirement(leaderboard, 'member') const leaderboardReversed = isLeaderboardReversed(leaderboard) if ( (requirement === null) @@ -296,16 +400,44 @@ async function getApplicableAttributes(member: CleanMember): Promise<StringNumbe let leaderboardsCount: number = Object.keys(applicableAttributes).length - const leaderboardsCountRequirement: number = await getMemberLeaderboardRequirement('leaderboards_count') + const leaderboardsCountRequirement: number = await getLeaderboardRequirement('leaderboards_count', 'member') if ( (leaderboardsCountRequirement === null) || (leaderboardsCount > leaderboardsCountRequirement) ) { - // add 1 extra because this attribute also counts :) applicableAttributes['leaderboards_count'] = leaderboardsCount } + return applicableAttributes +} + +/** Get the attributes for the profile, but only ones that would put them on the top 100 for leaderboards */ +async function getApplicableProfileLeaderboardAttributes(profile: CleanFullProfile): Promise<StringNumber> { + const leaderboardAttributes = getProfileLeaderboardAttributes(profile) + const applicableAttributes = {} + + for (const [ leaderboard, attributeValue ] of Object.entries(leaderboardAttributes)) { + const requirement = await getLeaderboardRequirement(leaderboard, 'profile') + const leaderboardReversed = isLeaderboardReversed(leaderboard) + if ( + (requirement === null) + || (leaderboardReversed ? attributeValue < requirement : attributeValue > requirement) + ) { + applicableAttributes[leaderboard] = attributeValue + } + } + + + let leaderboardsCount: number = Object.keys(applicableAttributes).length + const leaderboardsCountRequirement: number = await getLeaderboardRequirement('leaderboards_count', 'member') + + if ( + (leaderboardsCountRequirement === null) + || (leaderboardsCount > leaderboardsCountRequirement) + ) { + applicableAttributes['leaderboards_count'] = leaderboardsCount + } return applicableAttributes } @@ -330,9 +462,9 @@ export async function updateDatabaseMember(member: CleanMember, profile: CleanFu if (debug) console.log('done constants..') - const leaderboardAttributes = await getApplicableAttributes(member) + const leaderboardAttributes = await getApplicableMemberLeaderboardAttributes(member) - if (debug) console.log('done getApplicableAttributes..', leaderboardAttributes, member.username, profile.name) + if (debug) console.log('done getApplicableMemberLeaderboardAttributes..', leaderboardAttributes, member.username, profile.name) await memberLeaderboardsCollection.updateOne( { @@ -368,14 +500,78 @@ export async function updateDatabaseMember(member: CleanMember, profile: CleanFu if (debug) console.log('added member to leaderboards', member.username, leaderboardAttributes) } -const leaderboardUpdateQueue = new Queue({ +/** + * Update the profiles's leaderboard data on the server if applicable. + * This will not also update the members, you have to call updateDatabaseMember separately for that + */ +export async function updateDatabaseProfile(profile: CleanFullProfile): Promise<void> { + if (debug) console.log('updateDatabaseProfile', profile.name) + if (!client) return // the db client hasn't been initialized + + // the profile's been updated too recently, just return + if (recentlyUpdated.get(profile.uuid + 'profile')) + return + // store the profile in recentlyUpdated so it cant update for 3 more minutes + recentlyUpdated.set(profile.uuid + 'profile', true) + + if (debug) console.log('adding profile to leaderboards', profile.name) + + const leaderboardAttributes = await getApplicableProfileLeaderboardAttributes(profile) + + if (debug) console.log('done getApplicableProfileLeaderboardAttributes..', leaderboardAttributes, profile.name) + + await profileLeaderboardsCollection.updateOne( + { + uuid: profile.uuid + }, + { + '$set': { + players: profile.members.map(p => p.uuid), + stats: leaderboardAttributes, + last_updated: new Date() + } + }, + { upsert: true } + ) + + // add the profile to the cached leaderboard without having to refetch it + for (const [ attributeName, attributeValue ] of Object.entries(leaderboardAttributes)) { + const existingRawLeaderboard = await fetchProfileLeaderboardRaw(attributeName) + const leaderboardReverse = isLeaderboardReversed(attributeName) + const newRawLeaderboard = existingRawLeaderboard + // remove the player from the leaderboard, if they're there + .filter(value => value.uuid !== profile.uuid) + .concat([{ + last_updated: new Date(), + stats: leaderboardAttributes, + uuid: profile.uuid, + players: profile.members.map(p => p.uuid) + }]) + .sort((a, b) => leaderboardReverse ? a.stats[attributeName] - b.stats[attributeName] : b.stats[attributeName] - a.stats[attributeName]) + .slice(0, 100) + cachedRawLeaderboards.set(attributeName, newRawLeaderboard) + } + + if (debug) console.log('added profile to leaderboards', profile.name, leaderboardAttributes) +} + +const leaderboardUpdateMemberQueue = new Queue({ concurrent: 1, interval: 500 }) +const leaderboardUpdateProfileQueue = new Queue({ + concurrent: 1, + interval: 2000 +}) /** Queue an update for the member's leaderboard data on the server if applicable */ export async function queueUpdateDatabaseMember(member: CleanMember, profile: CleanFullProfile): Promise<void> { - leaderboardUpdateQueue.enqueue(async() => await updateDatabaseMember(member, profile)) + leaderboardUpdateMemberQueue.enqueue(async() => await updateDatabaseMember(member, profile)) +} + +/** Queue an update for the profile's leaderboard data on the server if applicable */ +export async function queueUpdateDatabaseProfile(profile: CleanFullProfile): Promise<void> { + leaderboardUpdateProfileQueue.enqueue(async() => await updateDatabaseProfile(profile)) } @@ -393,7 +589,7 @@ async function removeBadMemberLeaderboardAttributes(): Promise<void> { const unsetValue = {} unsetValue[leaderboard] = '' const filter = {} - const requirement = await getMemberLeaderboardRequirement(leaderboard) + const requirement = await getLeaderboardRequirement(leaderboard, 'member') const leaderboardReversed = isLeaderboardReversed(leaderboard) if (requirement !== null) { filter[`stats.${leaderboard}`] = { |