diff --git a/server/src/lib/score-import/framework/common/converter-failures.ts b/server/src/lib/score-import/framework/common/converter-failures.ts index a74124de6..6c27a3b54 100644 --- a/server/src/lib/score-import/framework/common/converter-failures.ts +++ b/server/src/lib/score-import/framework/common/converter-failures.ts @@ -1,6 +1,5 @@ // Various Errors that can occur during processing. -// @todo #116 Resolve circular dependency between converter-failures.ts and import-types/common/types. import { ImportTypes } from "tachi-common"; import { ImportTypeDataMap, ImportTypeContextMap } from "../../import-types/common/types"; diff --git a/server/src/server/router/api/v1/import/router.test.ts b/server/src/server/router/api/v1/import/router.test.ts index 205a22e8f..a1cbdd5cd 100644 --- a/server/src/server/router/api/v1/import/router.test.ts +++ b/server/src/server/router/api/v1/import/router.test.ts @@ -332,3 +332,70 @@ t.test("POST /api/v1/import/file", async (t) => { t.end(); }); + +t.test("POST /api/v1/import/orphans", async (t) => { + const cookie = await CreateFakeAuthCookie(mockApi); + + t.beforeEach(ResetDBState); + + t.test("Should force a reprocessing of orphan scores.", async (t) => { + await db["orphan-scores"].insert([ + { + userID: 1, + timeInserted: 1000, + orphanID: "asdf", + importType: "ir/direct-manual", + errMsg: "foo", + context: { + game: "iidx", + playtype: "SP", + service: "foo", + version: null, + }, + data: { + score: 500, + lamp: "HARD CLEAR", + matchType: "songTitle", + identifier: "5.1.1.", + difficulty: "ANOTHER", + }, + }, + { + userID: 1, + timeInserted: 1000, + orphanID: "asdf2", + importType: "ir/direct-manual", + errMsg: "foo", + context: { + game: "iidx", + playtype: "SP", + service: "foo", + version: null, + }, + data: { + score: 500, + lamp: "HARD CLEAR", + matchType: "songTitle", + identifier: "TITLE NOBODY WILL USE", + difficulty: "ANOTHER", + }, + }, + ]); + + const res = await mockApi.post("/api/v1/import/orphans").set("Cookie", cookie); + + t.equal(res.statusCode, 200, "Should return 200."); + + t.equal(res.body.body.success, 1, "Should successfully reprocess one orphan."); + t.equal(res.body.body.done, 2, "Should reprocess two orphans."); + t.equal(res.body.body.failed, 1, "Should fail in de-orphaning one orphan."); + + const dbCount = await db["orphan-scores"].count({}); + + t.equal(dbCount, 1, "Should only leave one orphan-score in the database."); + + t.end(); + }); + + t.end(); +}); diff --git a/server/src/server/router/api/v1/import/router.ts b/server/src/server/router/api/v1/import/router.ts index 75228efc6..f1c0778a6 100644 --- a/server/src/server/router/api/v1/import/router.ts +++ b/server/src/server/router/api/v1/import/router.ts @@ -1,7 +1,7 @@ import { Router } from "express"; import { APIImportTypes, FileUploadImportTypes, integer } from "tachi-common"; import Prudence from "prudence"; -import { GetUserWithIDGuaranteed } from "utils/user"; +import { FormatUserDoc, GetUserWithIDGuaranteed } from "utils/user"; import CreateLogCtx, { KtLogger } from "lib/logger/logger"; import prValidate from "server/middleware/prudence-validate"; import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error"; @@ -23,6 +23,8 @@ import { ParseFloSDVX } from "lib/score-import/import-types/api/flo-sdvx/parser" import { ParseMinSDVX } from "lib/score-import/import-types/api/min-sdvx/parser"; import { ParseArcSDVX } from "lib/score-import/import-types/api/arc-sdvx/parser"; import { ParseArcIIDX } from "lib/score-import/import-types/api/arc-iidx/parser"; +import db from "external/mongo/db"; +import { ReprocessOrphan } from "lib/score-import/framework/orphans/orphans"; const logger = CreateLogCtx(__filename); @@ -120,6 +122,62 @@ router.post( } ); +/** + * Force Tachi to reprocess your orphanned scores. This is automatically done + * daily, but this endpoint allows users to speed that up. + * + * @name POST /api/v1/import/orphans + */ +router.post("/orphans", RequirePermissions("submit_score"), async (req, res) => { + const userDoc = await GetUserWithIDGuaranteed(req.session.tachi!.user.id); + + logger.info(`User ${FormatUserDoc(userDoc)} forced an orphan sync.`); + + const orphans = await db["orphan-scores"].find({ + userID: userDoc.id, + }); + + // ScoreIDs are essentially userID dependent, so this is fine. + const blacklist = (await db["score-blacklist"].find({ userID: userDoc.id })).map( + (e) => e.scoreID + ); + + let done = 0; + let failed = 0; + let success = 0; + let removed = 0; + + await Promise.all( + orphans.map((or) => + ReprocessOrphan(or, blacklist, logger).then((r) => { + done++; + if (r === null) { + removed++; + } else if (r === false) { + failed++; + } else { + success++; + } + }) + ) + ); + + logger.info(`Finished attempting deorphaning.`); + + logger.info(`Success: ${success} | Failed ${failed} | Removed ${removed}.`); + + return res.status(200).json({ + success: true, + description: `Reprocessed ${done} orphan scores.`, + body: { + done, + failed, + success, + removed, + }, + }); +}); + /** * Resolves the data from a file upload into an iterable, * The appropriate processing function to map that iterable over,