feat: rival-info table

This commit is contained in:
zkldi
2022-12-18 23:53:16 +00:00
parent a88903d28d
commit c4e8a88aad
4 changed files with 180 additions and 42 deletions
+123 -29
View File
@@ -1,12 +1,15 @@
import CreateLogCtx from "lib/logger/logger";
import { ServerConfig } from "lib/setup/config";
import { GetRivalUsers } from "lib/rivals/rivals";
import { ServerConfig, TachiConfig } from "lib/setup/config";
import { CreateSongMap } from "tachi-common";
import { GetRelevantSongsAndCharts } from "utils/db";
import {
GetFolderCharts,
GetFolderNamesInOrder,
GetFoldersFromTable,
GetTableForIDGuaranteed,
} from "utils/folder";
import { GetRecentUGPTScores } from "utils/queries/scores";
import path from "path";
import type { BMSTableEntry, BMSTableHead } from "bms-table-loader";
import type { Request, Response } from "express-serve-static-core";
@@ -14,6 +17,7 @@ import type {
ChartDocument,
FolderDocument,
Playtypes,
SongDocument,
TableDocument,
integer,
} from "tachi-common";
@@ -23,6 +27,33 @@ const logger = CreateLogCtx(__filename);
// Instead of just supporting existing tables, Tachi should also be able
// to emit its own, custom BMS tables. These may be dynamic.
function AppendAndConvertChartsToBMSBody(
body: Array<BMSTableEntry>,
charts: Array<ChartDocument<"bms:7K" | "bms:14K">>,
songMap: Map<integer, SongDocument>,
level: string
) {
for (const chart of charts) {
const song = songMap.get(chart.songID);
// if we've got metadata to add...
if (song) {
body.push({
level,
title: song.title,
artist: song.artist,
md5: chart.data.hashMD5,
});
} else {
logger.warn(`BMS Chart md5=${chart.data.hashMD5} has no parent song.`);
body.push({
level,
md5: chart.data.hashMD5,
});
}
}
}
/**
* Convert a table in Tachi into a bms header.json and body.json.
*/
@@ -59,32 +90,16 @@ export async function TachiTableToBMSTableJSON(
const charts = data.charts as Array<ChartDocument<"bms:7K">>;
const songMap = CreateSongMap(data.songs);
for (const chart of charts) {
const song = songMap.get(chart.songID);
// if we've got metadata to add...
if (song) {
body.push({
level: folder.title,
title: song.title,
artist: song.artist,
md5: chart.data.hashMD5,
});
} else {
logger.warn(`BMS Chart md5=${chart.data.hashMD5} has no parent song.`);
body.push({
level: folder.title,
md5: chart.data.hashMD5,
});
}
}
AppendAndConvertChartsToBMSBody(body, charts, songMap, folder.title);
}
return body;
}
export type TachiBMSTable = {
playtype: Playtypes["bms"];
playtype: Playtypes["bms"] | null; // what playtype is this for? If null, this table
// is for all playtypes.
urlName: string; // what do we call this in the url?
tableName: string; // what should it be called in-game?
symbol: string; // what symbol should this table have?
@@ -93,13 +108,16 @@ export type TachiBMSTable = {
forSpecificUser: true; // if this table is user-dependent
// like, say, their rivals scores or something.
// then the callbacks need to recieve that info.
getLevelOrder: (userID: integer) => Promise<Array<string> | undefined>;
getBody: (userID: integer) => Promise<Array<BMSTableEntry>>;
getLevelOrder: (
userID: integer,
playtype: Playtypes["bms"]
) => Promise<Array<string> | undefined>;
getBody: (userID: integer, playtype: Playtypes["bms"]) => Promise<Array<BMSTableEntry>>;
}
| {
forSpecificUser?: false;
getLevelOrder: () => Promise<Array<string> | undefined>;
getBody: () => Promise<Array<BMSTableEntry>>;
getLevelOrder: (playtype: Playtypes["bms"]) => Promise<Array<string> | undefined>;
getBody: (playtype: Playtypes["bms"]) => Promise<Array<BMSTableEntry>>;
}
);
@@ -136,6 +154,14 @@ function GetUserID(req: Request) {
throw new Error(`No userID in params here. Is this route mounted in the right place?`);
}
function GetPlaytype(req: Request) {
if ("playtype" in req.params) {
return req.params.playtype as Playtypes["bms"];
}
throw new Error(`No playtype in params here. Is this route mounted in the right place?`);
}
/**
* Handle a request for a bms table. This endpoint should return "HTML" with the caveat
* that atleast one of the lines should refer to a "bmstable" meta header.
@@ -170,15 +196,16 @@ export async function HandleBMSTableHeaderRequest(
try {
let levelOrder;
let dataUrl;
const playtype = GetPlaytype(req);
if (bmsTable.forSpecificUser === true) {
const userID = GetUserID(req);
dataUrl = BMSTableToAbsoluteURL(bmsTable, "body", userID);
levelOrder = await bmsTable.getLevelOrder(userID);
levelOrder = await bmsTable.getLevelOrder(userID, playtype);
} else {
levelOrder = await bmsTable.getLevelOrder();
levelOrder = await bmsTable.getLevelOrder(playtype);
dataUrl = BMSTableToAbsoluteURL(bmsTable, "body", null);
}
@@ -207,12 +234,14 @@ export async function HandleBMSTableBodyRequest(
try {
let body;
const playtype = GetPlaytype(req);
if (bmsTable.forSpecificUser === true) {
const userID = GetUserID(req);
body = await bmsTable.getBody(userID);
body = await bmsTable.getBody(userID, playtype);
} else {
body = await bmsTable.getBody();
body = await bmsTable.getBody(playtype);
}
return res.status(200).send(body);
@@ -263,4 +292,69 @@ export const CUSTOM_TACHI_BMS_TABLES: Array<TachiBMSTable> = [
return GetFolderNamesInOrder(table);
},
},
{
urlName: "rival-info",
playtype: null,
symbol: "Rival",
tableName: `${TachiConfig.NAME} Rival Stats`,
forSpecificUser: true,
async getBody(userID, playtype) {
const rivals = await GetRivalUsers(userID, "bms", playtype);
const body: Array<BMSTableEntry> = [];
const promises = [];
for (const rival of rivals) {
promises.push(async () => {
const scores = await GetRecentUGPTScores(rival.id, "bms", playtype);
const data = await GetRelevantSongsAndCharts(scores, "bms");
const charts = data.charts as unknown as Array<
ChartDocument<"bms:7K" | "bms:14K">
>;
const songMap = CreateSongMap(data.songs);
AppendAndConvertChartsToBMSBody(
body,
charts,
songMap,
`${rival.username} Recent Plays`
);
});
promises.push(async () => {
const scores = await GetRecentUGPTScores(rival.id, "bms", playtype);
const data = await GetRelevantSongsAndCharts(scores, "bms");
const charts = data.charts as unknown as Array<
ChartDocument<"bms:7K" | "bms:14K">
>;
const songMap = CreateSongMap(data.songs);
AppendAndConvertChartsToBMSBody(
body,
charts,
songMap,
`${rival.username} Recent Highlights`
);
});
}
await Promise.all(promises);
return body;
},
async getLevelOrder(userID, playtype) {
const rivals = await GetRivalUsers(userID, "bms", playtype);
return rivals.flatMap((rival) => [
`${rival.username} Recent Plays`,
`${rival.username} Recent Highlights`,
]);
},
},
];
@@ -45,7 +45,7 @@ const FindCustomBMSTable: RequestHandler = (req, res, next) => {
/**
* Return some HTML for this custom table.
*
* @note Since this is the GPT route, trying to fetch user specific custom tables
* @note Since this is the UGPT route, trying to fetch GPT custom tables
* will result in a 404. This applies for all subsequent :tableUrlName routes.
*
* @name GET /api/v1/users/:userID/games/bms/:playtype/custom-tables/:tableUrlName
@@ -1,3 +1,13 @@
import {
GetGoalSummary,
GetRecentPlaycount,
GetRecentSessions,
GetRecentlyViewedFoldersAnyGPT,
} from "utils/queries/summary";
import { GetUser } from "utils/req-tachi-data";
import type { ProfileRatingAlgs } from "utils/string-checks";
import { FormatUserDoc, GetAllRankings, GetUserWithID } from "utils/user";
import { HashPassword, PasswordCompare, ValidatePassword } from "../../auth/auth";
import apiTokensRouter from "./api-tokens/router";
import bannerRouter from "./banner/router";
import followingRouter from "./following/router";
@@ -10,7 +20,6 @@ import { GetUserFromParam, RequireSelfRequestFromUser } from "./middleware";
import notifsRouter from "./notifications/router";
import pfpRouter from "./pfp/router";
import settingsRouter from "./settings/router";
import { HashPassword, PasswordCompare, ValidatePassword } from "../../auth/auth";
import { Router } from "express";
import db from "external/mongo/db";
import { GetRecentActivity } from "lib/activity/activity";
@@ -21,17 +30,7 @@ import p from "prudence";
import prValidate from "server/middleware/prudence-validate";
import { DeleteUndefinedProps, IsNonEmptyString, StripUrl } from "utils/misc";
import { optNullFluffStrField } from "utils/prudence";
import {
GetGoalSummary,
GetRecentlyViewedFoldersAnyGPT,
GetRecentPlaycount,
GetRecentSessions,
} from "utils/queries/summary";
import { GetUser } from "utils/req-tachi-data";
import { FormatUserDoc, GetAllRankings, GetUserWithID } from "utils/user";
import type { ActivityConstraint } from "lib/activity/activity";
import type { IDStrings, ImportTypes, integer, UserGameStats } from "tachi-common";
import type { ProfileRatingAlgs } from "utils/string-checks";
import type { IDStrings, ImportTypes, UserGameStats, integer } from "tachi-common";
const logger = CreateLogCtx(__filename);
+45
View File
@@ -0,0 +1,45 @@
import db from "external/mongo/db";
import type { Game, Playtype, integer } from "tachi-common";
export async function GetRecentUGPTScores(
userID: integer,
game: Game,
playtype: Playtype,
limit = 100
) {
return db.scores.find(
{
userID,
game,
playtype,
},
{
sort: {
timeAdded: -1,
},
limit,
}
);
}
export async function GetRecentUGPTHighlights(
userID: integer,
game: Game,
playtype: Playtype,
limit = 100
) {
return db.scores.find(
{
userID,
game,
playtype,
highlight: true,
},
{
sort: {
timeAdded: -1,
},
limit,
}
);
}