diff --git a/server/package.json b/server/package.json index aee1e9ce6..822634212 100644 --- a/server/package.json +++ b/server/package.json @@ -73,7 +73,7 @@ "redis": "3.1.2", "rimraf": "3.0.2", "safe-json-stringify": "1.2.0", - "tachi-common": "0.2.3", + "tachi-common": "0.2.4", "typescript": "4.3.4", "winston": "3.3.3" }, diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index cdbcc1794..e1e3873e7 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -54,7 +54,7 @@ specifiers: rimraf: 3.0.2 safe-json-stringify: 1.2.0 supertest: 6.1.3 - tachi-common: 0.2.3 + tachi-common: 0.2.4 tap: 15.0.9 ts-node: 10.0.0 tsconfig-paths: 3.10.1 @@ -88,7 +88,7 @@ dependencies: redis: 3.1.2 rimraf: 3.0.2 safe-json-stringify: 1.2.0 - tachi-common: 0.2.3_ts-node@10.0.0+typescript@4.3.4 + tachi-common: 0.2.4_ts-node@10.0.0+typescript@4.3.4 typescript: 4.3.4 winston: 3.3.3 @@ -3681,8 +3681,8 @@ packages: strip-ansi: 6.0.0 dev: true - /tachi-common/0.2.3_ts-node@10.0.0+typescript@4.3.4: - resolution: {integrity: sha512-l1w5IQfaZejgl/+opB2K5mRkeQstreOq4+6+9A1kkGNVFu0lxvLYZdqSlJwbqYVAPOO7ssYZsi+FSUf81zowpg==} + /tachi-common/0.2.4_ts-node@10.0.0+typescript@4.3.4: + resolution: {integrity: sha512-5Q6c0qGhQyPKKiCg+BvuYbt9k5q/B1c3aBNR3KaX+aEKSQ8ZbuYTcKPKnQlDccjDfcGNj2OAmELx3oHz4UJdGA==} dependencies: monk: 7.3.4 tap: 15.0.9_ts-node@10.0.0+typescript@4.3.4 diff --git a/server/scripts/set-counters.ts b/server/scripts/set-counters.ts index 294e9f8e8..024f14a91 100644 --- a/server/scripts/set-counters.ts +++ b/server/scripts/set-counters.ts @@ -3,6 +3,7 @@ import db from "external/mongo/db"; (async () => { const uscSongCount = await db.songs.usc.find({}); + const bmsSongCount = await db.songs.bms.find({}); const Counters: CounterDocument[] = [ { @@ -13,6 +14,10 @@ import db from "external/mongo/db"; counterName: "usc-song-id", value: uscSongCount.length + 1, }, + { + counterName: "bms-song-id", + value: bmsSongCount.length + 1, + }, ]; db.counters.insert(Counters).then(() => { diff --git a/server/src/external/mongo/db.ts b/server/src/external/mongo/db.ts index 249cc111f..6988848e9 100644 --- a/server/src/external/mongo/db.ts +++ b/server/src/external/mongo/db.ts @@ -34,7 +34,7 @@ import { OAuth2ApplicationDocument, integer, FervidexSettingsDocument, - USCOrphanChart, + OrphanChart, } from "tachi-common"; import monk, { TMiddleware } from "monk"; import CreateLogCtx from "lib/logger/logger"; @@ -178,7 +178,7 @@ const db = { // i've inlined this one because i don't see it appearing anywhere else. monkDB.get<{ code: string; userID: integer; createdOn: number }>("oauth2-auth-codes"), "fer-settings": monkDB.get("fer-settings"), - "usc-orphan-chart-queue": monkDB.get("usc-orphan-chart-queue"), + "orphan-chart-queue": monkDB.get("orphan-chart-queue"), }; export type StaticDatabases = @@ -210,7 +210,7 @@ export type StaticDatabases = | "oauth2-clients" | "oauth2-auth-codes" | "fer-settings" - | "usc-orphan-chart-queue" + | "orphan-chart-queue" | "user-settings"; export type Databases = StaticDatabases | `songs-${Game}` | `charts-${Game}`; diff --git a/server/src/lib/orphan-queue/orphan-queue.ts b/server/src/lib/orphan-queue/orphan-queue.ts new file mode 100644 index 000000000..ddc66a559 --- /dev/null +++ b/server/src/lib/orphan-queue/orphan-queue.ts @@ -0,0 +1,104 @@ +import db from "external/mongo/db"; +import CreateLogCtx from "lib/logger/logger"; +import { + ChartDocument, + IDStrings, + IDStringToGame, + integer, + OrphanChart, + SongDocument, +} from "tachi-common"; +import { GetNextCounterValue } from "utils/db"; +import { FilterQuery } from "mongodb"; + +const logger = CreateLogCtx(__filename); + +/** + * Handles an orphan queue request. + * + * If the chart has never been seen before, add it to the orphan queue + * and return null. + * + * If the chart has been seen before, and has less than N unique players + * who have played it, return null. + * + * If the chart has been seen before, and has >= N unique players who have + * played it, unorphan the chart, and return it. + */ +export async function HandleOrphanQueue( + idString: I, + game: IDStringToGame[I], + chartDoc: ChartDocument, + songDoc: SongDocument, + orphanMatchCriteria: FilterQuery>, + queueSize: integer, + userID: integer, + chartName: string +) { + logger.debug(`Recieved orphanqueue request for ${chartName}.`); + + const orphanChart = await db["orphan-chart-queue"].findOne( + Object.assign({ idString }, orphanMatchCriteria), + { + projectID: true, + } + ); + + if (!orphanChart) { + logger.verbose(`Recieved unknown chart ${chartName}, orphaning.`); + + await db["orphan-chart-queue"].insert({ + idString, + chartDoc, + songDoc, + userIDs: [userID], + }); + + return null; + } + + orphanChart.userIDs.push(userID); + + const uniqueUsersArr = [...new Set(orphanChart.userIDs)]; + + const playcount = uniqueUsersArr.length; + // If N or more people have played this chart while orphaned, unorphan + // it. + if (playcount >= queueSize) { + logger.info( + `Song ${chartName} was unorphaned by ${uniqueUsersArr.join(", ")} and ${userID}.` + ); + const songID = await GetNextCounterValue(`${game}-song-id`); + + logger.verbose(`${chartName} has been assigned songID ${songID}.`); + + const { songDoc, chartDoc } = orphanChart; + + songDoc.id = songID; + chartDoc.songID = songID; + + await db.songs[game].insert(songDoc); + await db.charts[game].insert(chartDoc); + await db["orphan-chart-queue"].remove({ + _id: orphanChart._id, + }); + + return chartDoc as ChartDocument; + } + + // otherwise, update the state of this orphan. + + logger.verbose(`UserID ${userID} played ${chartName}, which is now at ${playcount} plays.`); + await db["orphan-chart-queue"].update( + { + _id: orphanChart._id, + }, + { + $set: { + userIDs: uniqueUsersArr, + }, + } + ); + + return null; +} diff --git a/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts b/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts index 9e7f64054..734176059 100644 --- a/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts +++ b/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts @@ -8,8 +8,11 @@ import { import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; import { DryScore } from "../../../framework/common/types"; import { ConverterFunction } from "../../common/types"; -import { BeatorajaContext, BeatorajaScore } from "./types"; -import { ChartDocument } from "tachi-common"; +import { BeatorajaContext, BeatorajaScore, BeatorajaChart } from "./types"; +import { ChartDocument, SongDocument } from "tachi-common"; +import { HandleOrphanQueue } from "lib/orphan-queue/orphan-queue"; +import { Random20Hex } from "utils/misc"; +import { ServerConfig, ServerTypeInfo } from "lib/setup/config"; const LAMP_LOOKUP = { NoPlay: "NO PLAY", @@ -41,19 +44,50 @@ export const ConverterIRBeatoraja: ConverterFunction | null; if (!chart) { - // @todo #141 Import charts into the Kamaitachi Database if the chart - // doesn't exist. - throw new KTDataNotFoundFailure( - `Could not find chart ${context.chart.title}.`, - importType, - data, - context + const chartName = `${context.chart.artist} (${context.chart.subartist})- ${context.chart.title} (${context.chart.subtitle})`; + + if (context.chart.hasRandom) { + // If you're someone forking tachi looking to remove this + // check, remember to change the entire score import + // framework and database to be able to handle variable notecounts. + logger.verbose(`Declined to orphan chart ${chartName} as it has #RANDOM declarations.`); + throw new InvalidScoreFailure( + `${ServerTypeInfo.name} will not support #RANDOM charts.` + ); + } + + const idString = context.chart.mode === "BEAT_7K" ? "bms:7K" : "bms:14K"; + + const { chartDoc, songDoc } = ConvertBeatorajaChartToTachi(context.chart); + + chart = await HandleOrphanQueue( + idString, + "bms", + chartDoc, + songDoc, + { + "chartDoc.data.hashSHA256": context.chart.sha256, + }, + ServerConfig.BEATORAJA_QUEUE_SIZE, + context.userID, + chartName ); + + // If chart wasn't unorphaned as a result of this request + // orphan this score and return ktdnf + if (!chart) { + throw new KTDataNotFoundFailure( + `This chart is orphaned.`, + "ir/beatoraja", + data, + context + ); + } } const song = await FindSongOnID("bms", chart.songID); @@ -127,3 +161,39 @@ export const ConverterIRBeatoraja: ConverterFunction = { + chartID: Random20Hex(), + difficulty: "CHART", + isPrimary: true, + level: "?", + levelNum: 0, + playtype: chart.mode === "BEAT_7K" ? "7K" : "14K", + rgcID: null, + songID: 0, + versions: [], + data: { + hashMD5: chart.md5, + hashSHA256: chart.sha256, + notecount: chart.notes, + tableFolders: [], + }, + }; + + const songDoc: SongDocument<"bms"> = { + artist: chart.artist, + title: chart.title, + firstVersion: "", + id: 0, + "alt-titles": [], + "search-titles": [], + data: { + genre: chart.genre, + subartist: chart.subartist, + subtitle: chart.subtitle, + }, + }; + + return { songDoc, chartDoc }; +} diff --git a/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts b/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts index af8e1e82b..ab85ab0e6 100644 --- a/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts +++ b/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts @@ -4,6 +4,7 @@ import ScoreImportFatalError from "../../../framework/score-importing/score-impo import { FormatPrError } from "utils/prudence"; import { BeatorajaChart, BeatorajaContext, BeatorajaScore } from "./types"; import { ParserFunctionReturns } from "../../common/types"; +import { integer } from "tachi-common"; const PR_BeatorajaScore = { sha256: "string", @@ -63,6 +64,7 @@ const PR_BeatorajaChart = { export function ParseBeatorajaSingle( body: Record, + userID: integer, logger: KtLogger ): ParserFunctionReturns { const err = p( @@ -104,6 +106,7 @@ export function ParseBeatorajaSingle( context: { client, chart: body.chart as BeatorajaChart, + userID, }, game: "bms", iterable: [body.score] as unknown as BeatorajaScore[], diff --git a/server/src/lib/score-import/import-types/ir/beatoraja/types.ts b/server/src/lib/score-import/import-types/ir/beatoraja/types.ts index 175d5d1a5..5380fc747 100644 --- a/server/src/lib/score-import/import-types/ir/beatoraja/types.ts +++ b/server/src/lib/score-import/import-types/ir/beatoraja/types.ts @@ -3,6 +3,7 @@ import { integer } from "tachi-common"; export interface BeatorajaContext { client: "lr2oraja"; chart: BeatorajaChart; + userID: integer; // unexpectedly necessary for orphan code! } export interface BeatorajaChart { @@ -18,6 +19,7 @@ export interface BeatorajaChart { mode: "BEAT_7K" | "BEAT_14K"; judge: number; notes: integer; + hasRandom: boolean; } export interface BeatorajaScore { diff --git a/server/src/lib/setup/config.ts b/server/src/lib/setup/config.ts index 2e1a01599..ca8139074 100644 --- a/server/src/lib/setup/config.ts +++ b/server/src/lib/setup/config.ts @@ -73,6 +73,7 @@ export interface TachiConfig { SENDMAIL_BIN?: string; }; USC_QUEUE_SIZE: integer; + BEATORAJA_QUEUE_SIZE: integer; } const isValidOauth2 = p.optional({ @@ -109,6 +110,7 @@ const err = p(config, { SENDMAIL_BIN: "*string", }), USC_QUEUE_SIZE: p.optional(p.gteInt(2)), + BEATORAJA_QUEUE_SIZE: p.optional(p.gteInt(2)), }); if (err) { @@ -129,6 +131,7 @@ const tachiConfig = config as TachiConfig; tachiConfig.RATE_LIMIT ??= 500; tachiConfig.OAUTH_CLIENT_CAP ??= 15; tachiConfig.USC_QUEUE_SIZE ??= 3; +tachiConfig.BEATORAJA_QUEUE_SIZE ??= 3; if (tachiConfig.EMAIL_CONFIG) { tachiConfig.EMAIL_CONFIG.SENDMAIL_BIN ??= "/usr/bin/sendmail"; diff --git a/server/src/server/router/ir/beatoraja/charts/router.ts b/server/src/server/router/ir/beatoraja/charts/router.ts index 78edacc63..5d73c8a05 100644 --- a/server/src/server/router/ir/beatoraja/charts/router.ts +++ b/server/src/server/router/ir/beatoraja/charts/router.ts @@ -28,6 +28,7 @@ router.use(GetChartDocument); /** * Retrieves scores for the given chart. + * * @name GET /ir/beatoraja/chart/:chartSHA256/scores */ router.get("/scores", async (req, res) => { diff --git a/server/src/server/router/ir/beatoraja/router.test.ts b/server/src/server/router/ir/beatoraja/router.test.ts index 36f11346e..5edc86e5e 100644 --- a/server/src/server/router/ir/beatoraja/router.test.ts +++ b/server/src/server/router/ir/beatoraja/router.test.ts @@ -5,6 +5,7 @@ import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { GetKTDataJSON } from "test-utils/test-data"; import deepmerge from "deepmerge"; +import { PublicUserDocument } from "tachi-common"; t.test("POST /ir/beatoraja/submit-score", (t) => { t.beforeEach(ResetDBState); @@ -103,6 +104,132 @@ t.test("POST /ir/beatoraja/submit-score", (t) => { t.end(); }); + t.test("Should defer a chart to the orphan queue if not found.", async (t) => { + const res = await mockApi + .post("/ir/beatoraja/submit-score") + .set("X-TachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send( + deepmerge(scoreReq, { + chart: { sha256: "new_chart", md5: "new_md5" }, + score: { sha256: "new_chart", md5: "new_md5" }, + }) + ); + + t.equal(res.status, 202); + + t.equal(res.body.success, true); + t.match(res.body.description, /Chart and score have been orphaned/u); + + const orphanChart = await db["orphan-chart-queue"].findOne({ + "chartDoc.data.hashSHA256": "new_chart", + }); + + t.hasStrict(orphanChart?.chartDoc, { + data: { + hashSHA256: "new_chart", + hashMD5: "new_md5", + }, + }); + + t.end(); + }); + + t.test("Should eventually unorphan a chart.", async (t) => { + await db["api-tokens"].insert([ + { + userID: 2, + identifier: "token2", + permissions: { submit_score: true }, + token: "token2", + }, + { + userID: 3, + identifier: "token3", + permissions: { submit_score: true }, + token: "token3", + }, + ]); + + await db.users.insert([ + { + id: 2, + username: "foo", + usernameLowercase: "foo", + }, + { + id: 3, + username: "bar", + usernameLowercase: "bar", + }, + ] as PublicUserDocument[]); + + const res = await mockApi + .post("/ir/beatoraja/submit-score") + .set("X-TachiIR-Version", "2.0.0") + .set("Authorization", "Bearer mock_token") + .send( + deepmerge(scoreReq, { + chart: { sha256: "new_chart", md5: "new_md5" }, + score: { sha256: "new_chart", md5: "new_md5" }, + }) + ); + + t.equal(res.statusCode, 202); + + const res2 = await mockApi + .post("/ir/beatoraja/submit-score") + .set("X-TachiIR-Version", "2.0.0") + .set("Authorization", "Bearer token2") + .send( + deepmerge(scoreReq, { + chart: { sha256: "new_chart", md5: "new_md5" }, + score: { sha256: "new_chart", md5: "new_md5" }, + }) + ); + + t.equal(res2.statusCode, 202); + + const orphanData = await db["orphan-chart-queue"].findOne({ + "chartDoc.data.hashSHA256": "new_chart", + }); + + t.strictSame(orphanData?.userIDs, [1, 2]); + + const res3 = await mockApi + .post("/ir/beatoraja/submit-score") + .set("X-TachiIR-Version", "2.0.0") + .set("Authorization", "Bearer token3") + .send( + deepmerge(scoreReq, { + chart: { sha256: "new_chart", md5: "new_md5" }, + score: { sha256: "new_chart", md5: "new_md5" }, + }) + ); + + t.equal(res3.statusCode, 200); + + const orphanData2 = await db["orphan-chart-queue"].findOne({ + "chartDoc.data.hashSHA256": "new_chart", + }); + + t.equal(orphanData2, null, "Orphan data should be removed from the database."); + + const score = await db.scores.findOne({ + game: "bms", + userID: 3, + }); + + t.hasStrict(score, { + scoreData: { + score: 1004, + }, + importType: "ir/beatoraja", + }); + + t.end(); + }); + t.end(); }); diff --git a/server/src/server/router/ir/beatoraja/router.ts b/server/src/server/router/ir/beatoraja/router.ts index 8cba74500..93b2d83dc 100644 --- a/server/src/server/router/ir/beatoraja/router.ts +++ b/server/src/server/router/ir/beatoraja/router.ts @@ -4,6 +4,7 @@ import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; import CreateLogCtx, { KtLogger } from "lib/logger/logger"; import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper"; import { ParseBeatorajaSingle } from "lib/score-import/import-types/ir/beatoraja/parser"; +import { ServerConfig } from "lib/setup/config"; import { UpdateClassIfGreater } from "utils/class"; import { GetUserWithIDGuaranteed } from "utils/user"; import { ValidateIRClientVersion } from "./auth"; @@ -16,14 +17,14 @@ const router: Router = Router({ mergeParams: true }); router.use(ValidateIRClientVersion); /** - * Submits a beatoraja score to Kamaitachi. If the chart is unavailable, - * store it as a new chart alongside the new score. + * Submits a beatoraja score to Tachi. + * * @name POST /ir/beatoraja/submit-score */ router.post("/submit-score", async (req, res) => { const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth]!.userID!); - const ParserFunction = (logger: KtLogger) => ParseBeatorajaSingle(req.body, logger); + const ParserFunction = (logger: KtLogger) => ParseBeatorajaSingle(req.body, userDoc.id, logger); const importRes = await ExpressWrappedScoreImportMain( userDoc, @@ -35,6 +36,13 @@ router.post("/submit-score", async (req, res) => { if (!importRes.body.success) { return res.status(400).json(importRes.body); } else if (importRes.body.body.errors.length !== 0) { + if (importRes.body.body.errors[0].type === "KTDataNotFound") { + return res.status(202).json({ + success: true, + description: `Chart and score have been orphaned. This score will reify when atleast ${ServerConfig.BEATORAJA_QUEUE_SIZE} players have played the chart.`, + }); + } + // since we're only ever importing one score, we can guarantee // that this means the score we tried to import was skipped. return res.status(400).json({ @@ -78,8 +86,9 @@ router.post("/submit-score", async (req, res) => { }); /** - * Submits a course result to Kamaitachi. This only accepts a limited set of + * Submits a course result to Tachi. This only accepts a limited set of * courses - all of which are dans. + * * @name POST /ir/beatoraja/submit-course */ router.post("/submit-course", async (req, res) => { diff --git a/server/src/server/router/ir/usc/orphan-queue.ts b/server/src/server/router/ir/usc/orphan-queue.ts deleted file mode 100644 index 9ff2e0e9d..000000000 --- a/server/src/server/router/ir/usc/orphan-queue.ts +++ /dev/null @@ -1,118 +0,0 @@ -import db from "external/mongo/db"; -import CreateLogCtx from "lib/logger/logger"; -import { ServerConfig } from "lib/setup/config"; -import { ChartDocument, integer, SongDocument } from "tachi-common"; -import { GetNextCounterValue } from "utils/db"; -import { Random20Hex } from "utils/misc"; -import { USCClientChart } from "./types"; - -const logger = CreateLogCtx(__filename); - -/** - * Handles the orphan queue for USC charts. If the provided chart was just - * unorphaned, returns the chart. Else, returns null. - */ -export async function HandleOrphanQueue(uscChartDoc: USCClientChart, userID: integer) { - const chartName = `${uscChartDoc.artist} - ${uscChartDoc.title} (${IndexToDiff( - uscChartDoc.difficulty - )})`; - - logger.debug(`Recieved orphanqueue request for ${chartName}.`); - - const orphanChart = await db["usc-orphan-chart-queue"].findOne({ - "chartDoc.data.hashSHA1": uscChartDoc.chartHash, - }); - - if (!orphanChart) { - logger.verbose(`Recieved unknown chart ${chartName}, orphaning.`); - - const { song, chart } = ConvertUSCChart(uscChartDoc); - - await db["usc-orphan-chart-queue"].insert({ - chartDoc: chart, - songDoc: song, - userIDs: [userID], - }); - - return null; - } - - orphanChart.userIDs.push(userID); - - const uniqueUsersArr = [...new Set(orphanChart.userIDs)]; - - const playcount = uniqueUsersArr.length; - // If N or more people have played this chart while orphaned, unorphan - // it. - if (playcount >= ServerConfig.USC_QUEUE_SIZE) { - logger.info( - `Song ${chartName} was unorphaned by ${uniqueUsersArr.join(", ")} and ${userID}.` - ); - const songID = await GetNextCounterValue("usc-song-id"); - - logger.verbose(`${chartName} has been assigned songID ${songID}.`); - - const { songDoc, chartDoc } = orphanChart; - - songDoc.id = songID; - chartDoc.songID = songID; - - await db.songs.usc.insert(songDoc); - await db.charts.usc.insert(chartDoc); - await db["usc-orphan-chart-queue"].remove({ - "chartDoc.data.hashSHA1": chartDoc.data.hashSHA1, - }); - - return chartDoc; - } - - // otherwise, update the state of this orphan. - - logger.verbose(`UserID ${userID} played ${chartName}, which is now at ${playcount} plays.`); - await db["usc-orphan-chart-queue"].update( - { - "chartDoc.data.hashSHA1": uscChartDoc.chartHash, - }, - { - $set: { - userIDs: uniqueUsersArr, - }, - } - ); - - return null; -} - -function ConvertUSCChart(uscChartDoc: USCClientChart) { - const chart: ChartDocument<"usc:Single"> = { - chartID: Random20Hex(), - difficulty: IndexToDiff(uscChartDoc.difficulty), - isPrimary: true, - level: "?", - levelNum: 0, - playtype: "Single", - rgcID: null, - songID: 0, - versions: [], - data: { - hashSHA1: uscChartDoc.chartHash, - isOfficial: false, - }, - }; - - const song: SongDocument<"usc"> = { - title: uscChartDoc.title, - artist: uscChartDoc.artist, - firstVersion: null, - id: 0, - data: {}, - "alt-titles": [], - "search-titles": [], - }; - - return { chart, song }; -} - -function IndexToDiff(index: 0 | 1 | 2 | 3): ChartDocument<"usc:Single">["difficulty"] { - return (["NOV", "ADV", "EXH", "INF"] as const)[index]; -} diff --git a/server/src/server/router/ir/usc/router.test.ts b/server/src/server/router/ir/usc/router.test.ts index b0c2a62da..9b62c4a12 100644 --- a/server/src/server/router/ir/usc/router.test.ts +++ b/server/src/server/router/ir/usc/router.test.ts @@ -551,7 +551,7 @@ t.test("POST /scores", async (t) => { t.equal(dbScore, null, "Should not exist in db."); - const orphanData = await db["usc-orphan-chart-queue"].findOne({ + const orphanData = await db["orphan-chart-queue"].findOne({ "chartDoc.data.hashSHA1": "NEW_CHART", }); @@ -636,7 +636,7 @@ t.test("POST /scores", async (t) => { t.equal(res2.body.statusCode, 22); - const orphanData = await db["usc-orphan-chart-queue"].findOne({ + const orphanData = await db["orphan-chart-queue"].findOne({ "chartDoc.data.hashSHA1": "NEW_CHART", }); @@ -655,7 +655,7 @@ t.test("POST /scores", async (t) => { t.equal(res3.body.statusCode, 20); - const orphanData2 = await db["usc-orphan-chart-queue"].findOne({ + const orphanData2 = await db["orphan-chart-queue"].findOne({ "chartDoc.data.hashSHA1": "NEW_CHART", }); diff --git a/server/src/server/router/ir/usc/router.ts b/server/src/server/router/ir/usc/router.ts index b2ec687b1..41f4dd3f8 100644 --- a/server/src/server/router/ir/usc/router.ts +++ b/server/src/server/router/ir/usc/router.ts @@ -1,6 +1,5 @@ import { Router, RequestHandler } from "express"; import p from "prudence"; -import { FindChartOnSHA256 } from "utils/queries/charts"; import { SYMBOL_TachiAPIAuth, SYMBOL_TachiData } from "lib/constants/tachi"; import db from "external/mongo/db"; import { @@ -11,7 +10,12 @@ import { } from "tachi-common"; import { AssertStrAsPositiveNonZeroInt } from "lib/score-import/framework/common/string-asserts"; import CreateLogCtx, { KtLogger } from "lib/logger/logger"; -import { CreatePOSTScoresResponseBody, TachiScoreToServerScore } from "./usc"; +import { + ConvertUSCChart, + CreatePOSTScoresResponseBody, + TachiScoreToServerScore, + USCChartIndexToDiff, +} from "./usc"; import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper"; import { GetUserWithID } from "utils/user"; import { ParseIRUSC } from "lib/score-import/import-types/ir/usc/parser"; @@ -24,7 +28,8 @@ import { RequirePermissions } from "server/middleware/auth"; import { GetUSCIRReplayURL } from "lib/cdn/url-format"; import { FormatPrError } from "utils/prudence"; import { USCClientChart } from "./types"; -import { HandleOrphanQueue } from "./orphan-queue"; +import { HandleOrphanQueue } from "lib/orphan-queue/orphan-queue"; +import { ServerConfig } from "lib/setup/config"; const logger = CreateLogCtx(__filename); @@ -280,7 +285,24 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => { // If this chart is already orphaned, increase its unique player // playcount. if (!chartDoc) { - chartDoc = await HandleOrphanQueue(uscChart, req[SYMBOL_TachiAPIAuth].userID!); + const { song, chart } = ConvertUSCChart(uscChart); + + const uscChartName = `${uscChart.artist} - ${uscChart.title} (${USCChartIndexToDiff( + uscChart.difficulty + )})`; + + chartDoc = await HandleOrphanQueue( + "usc:Single", + "usc", + chart, + song, + { + "chartDoc.data.hashSHA1": uscChart.chartHash, + }, + ServerConfig.USC_QUEUE_SIZE, + req[SYMBOL_TachiAPIAuth].userID!, + uscChartName + ); } const userDoc = await GetUserWithID(req[SYMBOL_TachiAPIAuth]!.userID!); diff --git a/server/src/server/router/ir/usc/usc.ts b/server/src/server/router/ir/usc/usc.ts index a46f49a55..e1d149098 100644 --- a/server/src/server/router/ir/usc/usc.ts +++ b/server/src/server/router/ir/usc/usc.ts @@ -1,10 +1,10 @@ -import { integer, PBScoreDocument, ScoreDocument, ChartDocument } from "tachi-common"; +import { integer, PBScoreDocument, ScoreDocument, ChartDocument, SongDocument } from "tachi-common"; import CreateLogCtx from "lib/logger/logger"; import { GetPBOnChart, GetServerRecordOnChart } from "utils/scores"; -import { MStoS } from "utils/misc"; +import { MStoS, Random20Hex } from "utils/misc"; import { USCIR_ADJACENT_SCORE_N } from "lib/constants/usc-ir"; import db from "external/mongo/db"; -import { USCServerScore } from "./types"; +import { USCClientChart, USCServerScore } from "./types"; const logger = CreateLogCtx(__filename); @@ -201,3 +201,39 @@ export interface POSTScoresResponseBody { isServerRecord: boolean; sendReplay: string; } + +export function ConvertUSCChart(uscChartDoc: USCClientChart) { + const chart: ChartDocument<"usc:Single"> = { + chartID: Random20Hex(), + difficulty: USCChartIndexToDiff(uscChartDoc.difficulty), + isPrimary: true, + level: "?", + levelNum: 0, + playtype: "Single", + rgcID: null, + songID: 0, + versions: [], + data: { + hashSHA1: uscChartDoc.chartHash, + isOfficial: false, + }, + }; + + const song: SongDocument<"usc"> = { + title: uscChartDoc.title, + artist: uscChartDoc.artist, + firstVersion: null, + id: 0, + data: {}, + "alt-titles": [], + "search-titles": [], + }; + + return { chart, song }; +} + +export function USCChartIndexToDiff( + index: 0 | 1 | 2 | 3 +): ChartDocument<"usc:Single">["difficulty"] { + return (["NOV", "ADV", "EXH", "INF"] as const)[index]; +} diff --git a/server/src/test-utils/mock-db/counters.json b/server/src/test-utils/mock-db/counters.json index 5c28b33ec..b7587808e 100644 --- a/server/src/test-utils/mock-db/counters.json +++ b/server/src/test-utils/mock-db/counters.json @@ -10,5 +10,9 @@ { "counterName": "usc-song-id", "value": 2 + }, + { + "counterName": "bms-song-id", + "value": 2 } ] \ No newline at end of file diff --git a/server/src/test-utils/mock-db/orphan-chart-queue.json b/server/src/test-utils/mock-db/orphan-chart-queue.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/server/src/test-utils/mock-db/orphan-chart-queue.json @@ -0,0 +1 @@ +[] \ No newline at end of file