cleanup and track player history when creating the player instead of waiting for the cron job
All checks were successful
Deploy Backend / docker (ubuntu-latest) (push) Successful in 41s

This commit is contained in:
Lee 2024-10-27 14:01:12 +00:00
parent de3768559f
commit 28e8561020
2 changed files with 159 additions and 110 deletions

@ -0,0 +1,37 @@
import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token";
import { DiscordChannels, logToChannel } from "../bot/bot";
import { EmbedBuilder } from "discord.js";
import { formatPp } from "@ssr/common/utils/number-utils";
/**
* Logs that a new player is being tracked
*
* @param player the player being tracked
*/
export async function logNewTrackedPlayer(player: ScoreSaberPlayerToken) {
await logToChannel(
DiscordChannels.trackedPlayerLogs,
new EmbedBuilder()
.setTitle("New Player Tracked")
.setDescription(`https://ssr.fascinated.cc/player/${player.id}`)
.addFields([
{
name: "Username",
value: player.name,
inline: true,
},
{
name: "ID",
value: player.id,
inline: true,
},
{
name: "PP",
value: formatPp(player.pp) + "pp",
inline: true,
},
])
.setThumbnail(player.profilePicture)
.setColor("#00ff00")
);
}

@ -4,89 +4,88 @@ import { getDaysAgoDate, getMidnightAlignedDate } from "@ssr/common/utils/time-u
import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token"; import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token";
import { InternalServerError } from "@ssr/common/error/internal-server-error"; import { InternalServerError } from "@ssr/common/error/internal-server-error";
import { formatPp } from "@ssr/common/utils/number-utils";
import { delay, getPageFromRank, isProduction } from "@ssr/common/utils/utils"; import { delay, getPageFromRank, isProduction } from "@ssr/common/utils/utils";
import { DiscordChannels, logToChannel } from "../bot/bot";
import { EmbedBuilder } from "discord.js";
import { AroundPlayer } from "@ssr/common/types/around-player"; import { AroundPlayer } from "@ssr/common/types/around-player";
import { ScoreSort } from "@ssr/common/score/score-sort"; import { ScoreSort } from "@ssr/common/score/score-sort";
import { getScoreSaberLeaderboardFromToken } from "@ssr/common/token-creators"; import { getScoreSaberLeaderboardFromToken } from "@ssr/common/token-creators";
import { ScoreService } from "./score.service"; import { ScoreService } from "./score.service";
import { logNewTrackedPlayer } from "../common/embds";
const SCORESABER_REQUEST_COOLDOWN = 60_000 / 250; // 250 requests per minute
const accountCreationLock: { [id: string]: Promise<PlayerDocument> } = {};
export class PlayerService { export class PlayerService {
/**
* Get a player from the database.
*
* @param id the player to fetch
* @param create if true, create the player if it doesn't exist
* @param playerToken an optional player token for the player
* @returns the player
* @throws NotFoundError if the player is not found
*/
public static async getPlayer( public static async getPlayer(
id: string, id: string,
create: boolean = false, create: boolean = false,
playerToken?: ScoreSaberPlayerToken playerToken?: ScoreSaberPlayerToken
): Promise<PlayerDocument> { ): Promise<PlayerDocument> {
// Wait for the existing lock if it's in progress
if (accountCreationLock[id] !== undefined) {
await accountCreationLock[id];
}
let player: PlayerDocument | null = await PlayerModel.findById(id); let player: PlayerDocument | null = await PlayerModel.findById(id);
if (player === null) { if (player === null) {
// If create is on, create the player, otherwise return unknown player if (!create) {
playerToken = create ? (playerToken ? playerToken : await scoresaberService.lookupPlayer(id)) : undefined;
if (playerToken === undefined) {
throw new NotFoundError(`Player "${id}" not found`); throw new NotFoundError(`Player "${id}" not found`);
} }
console.log(`Creating player "${id}"...`); playerToken = playerToken || (await scoresaberService.lookupPlayer(id));
try {
player = (await PlayerModel.create({ _id: id })) as PlayerDocument;
player.trackedSince = new Date();
await this.seedPlayerHistory(player, playerToken);
// Only notify in production if (!playerToken) {
throw new NotFoundError(`Player "${id}" not found`);
}
// Create a new lock promise and assign it
accountCreationLock[id] = (async () => {
let newPlayer: PlayerDocument;
try {
console.log(`Creating player "${id}"...`);
newPlayer = (await PlayerModel.create({ _id: id })) as PlayerDocument;
newPlayer.trackedSince = new Date();
await newPlayer.save();
await this.seedPlayerHistory(newPlayer.id, playerToken);
await this.trackPlayerScores(newPlayer.id, SCORESABER_REQUEST_COOLDOWN);
// Notify in production
if (isProduction()) { if (isProduction()) {
await logToChannel( await logNewTrackedPlayer(playerToken);
DiscordChannels.trackedPlayerLogs,
new EmbedBuilder()
.setTitle("New Player Tracked")
.setDescription(`https://ssr.fascinated.cc/player/${playerToken.id}`)
.addFields([
{
name: "Username",
value: playerToken.name,
inline: true,
},
{
name: "ID",
value: playerToken.id,
inline: true,
},
{
name: "PP",
value: formatPp(playerToken.pp) + "pp",
inline: true,
},
])
.setThumbnail(playerToken.profilePicture)
.setColor("#00ff00")
);
} }
} catch (err) { } catch (err) {
const message = `Failed to create player document for "${id}"`; console.log(`Failed to create player document for "${id}"`, err);
console.log(message, err); throw new InternalServerError(`Failed to create player document for "${id}"`);
throw new InternalServerError(message); } finally {
// Ensure the lock is always removed
delete accountCreationLock[id];
} }
return newPlayer;
})();
// Wait for the player creation to complete
player = await accountCreationLock[id];
} }
return player;
// Ensure that the player is now of type PlayerDocument
return player as PlayerDocument;
} }
/** /**
* Seeds the player's history using data from * Seeds the player's history using data from
* the ScoreSaber API. * the ScoreSaber API.
* *
* @param player the player to seed * @param playerId the player id
* @param playerToken the SoreSaber player token * @param playerToken the SoreSaber player token
*/ */
public static async seedPlayerHistory(player: PlayerDocument, playerToken: ScoreSaberPlayerToken): Promise<void> { public static async seedPlayerHistory(playerId: string, playerToken: ScoreSaberPlayerToken): Promise<void> {
const player = await PlayerModel.findById(playerId);
if (player == null) {
throw new NotFoundError(`Player "${playerId}" not found`);
}
// Loop through rankHistory in reverse, from current day backwards // Loop through rankHistory in reverse, from current day backwards
const playerRankHistory = playerToken.histories.split(",").map((value: string) => { const playerRankHistory = playerToken.histories.split(",").map((value: string) => {
return parseInt(value); return parseInt(value);
@ -134,7 +133,7 @@ export class PlayerService {
// Seed the history with ScoreSaber data if no history exists // Seed the history with ScoreSaber data if no history exists
if (foundPlayer.getDaysTracked() === 0) { if (foundPlayer.getDaysTracked() === 0) {
await this.seedPlayerHistory(foundPlayer, player); await this.seedPlayerHistory(foundPlayer.id, player);
} }
// Update current day's statistics // Update current day's statistics
@ -248,16 +247,17 @@ export class PlayerService {
} }
/** /**
* Ensures all player scores are up-to-date. * Tracks a player's scores from the ScoreSaber API.
*
* @param playerId the player's id
* @param perPageCooldown the cooldown between pages
*/ */
public static async refreshPlayerScores() { public static async trackPlayerScores(playerId: string, perPageCooldown: number) {
const cooldown = 60_000 / 250; // 250 requests per minute const player = await PlayerModel.findById(playerId);
console.log(`Refreshing player score data...`); if (player == null) {
throw new NotFoundError(`Player "${playerId}" not found`);
}
const players = await PlayerModel.find({});
console.log(`Found ${players.length} players to refresh.`);
for (const player of players) {
console.log(`Refreshing scores for ${player.id}...`); console.log(`Refreshing scores for ${player.id}...`);
let page = 1; let page = 1;
let hasMorePages = true; let hasMorePages = true;
@ -298,7 +298,7 @@ export class PlayerService {
} }
page++; page++;
await delay(cooldown); // Cooldown between page requests await delay(perPageCooldown); // Cooldown between page requests
} }
// Mark player as seeded // Mark player as seeded
@ -306,7 +306,20 @@ export class PlayerService {
await player.save(); await player.save();
console.log(`Finished refreshing scores for ${player.id}, total pages refreshed: ${page - 1}.`); console.log(`Finished refreshing scores for ${player.id}, total pages refreshed: ${page - 1}.`);
await delay(cooldown); // Cooldown between players }
/**
* Ensures all player scores are up-to-date.
*/
public static async refreshPlayerScores() {
console.log(`Refreshing player score data...`);
const players = await PlayerModel.find({});
console.log(`Found ${players.length} players to refresh.`);
for (const player of players) {
await this.trackPlayerScores(player.id, SCORESABER_REQUEST_COOLDOWN);
await delay(SCORESABER_REQUEST_COOLDOWN); // Cooldown between players
} }
} }
@ -315,7 +328,6 @@ export class PlayerService {
*/ */
public static async updatePlayerStatistics() { public static async updatePlayerStatistics() {
const pages = 20; // top 1000 players const pages = 20; // top 1000 players
const cooldown = 60_000 / 250; // 250 requests per minute
let toTrack: PlayerDocument[] = await PlayerModel.find({}); let toTrack: PlayerDocument[] = await PlayerModel.find({});
const toRemoveIds: string[] = []; const toRemoveIds: string[] = [];
@ -328,7 +340,7 @@ export class PlayerService {
const page = await scoresaberService.lookupPlayers(pageNumber); const page = await scoresaberService.lookupPlayers(pageNumber);
if (page === undefined) { if (page === undefined) {
console.log(`Failed to fetch players on page ${pageNumber}, skipping page...`); console.log(`Failed to fetch players on page ${pageNumber}, skipping page...`);
await delay(cooldown); await delay(SCORESABER_REQUEST_COOLDOWN);
continue; continue;
} }
for (const player of page.players) { for (const player of page.players) {
@ -336,7 +348,7 @@ export class PlayerService {
await PlayerService.trackScoreSaberPlayer(foundPlayer, player); await PlayerService.trackScoreSaberPlayer(foundPlayer, player);
toRemoveIds.push(foundPlayer.id); toRemoveIds.push(foundPlayer.id);
} }
await delay(cooldown); await delay(SCORESABER_REQUEST_COOLDOWN);
} }
console.log(`Finished tracking player statistics for ${pages} pages, found ${toRemoveIds.length} players.`); console.log(`Finished tracking player statistics for ${pages} pages, found ${toRemoveIds.length} players.`);
@ -346,7 +358,7 @@ export class PlayerService {
console.log(`Tracking ${toTrack.length} player statistics...`); console.log(`Tracking ${toTrack.length} player statistics...`);
for (const player of toTrack) { for (const player of toTrack) {
await PlayerService.trackScoreSaberPlayer(player); await PlayerService.trackScoreSaberPlayer(player);
await delay(cooldown); await delay(SCORESABER_REQUEST_COOLDOWN);
} }
console.log("Finished tracking player statistics."); console.log("Finished tracking player statistics.");
} }