From 78c3dc533bee8fe67d29d15f24db314c2b6a4972 Mon Sep 17 00:00:00 2001 From: zkldi Date: Wed, 13 Oct 2021 00:16:49 +0100 Subject: [PATCH] Add blacklist support --- server/src/lib/delete-scores/delete-scores.ts | 120 +++--------------- .../framework/pb/create-pb-doc.ts | 8 +- .../score-importing/score-importing.ts | 36 +++++- .../api/v1/scores/_scoreID/router.test.ts | 75 +++++++++++ .../router/api/v1/scores/_scoreID/router.ts | 26 ++++ 5 files changed, 161 insertions(+), 104 deletions(-) diff --git a/server/src/lib/delete-scores/delete-scores.ts b/server/src/lib/delete-scores/delete-scores.ts index 0703c857f..ed3368ca9 100644 --- a/server/src/lib/delete-scores/delete-scores.ts +++ b/server/src/lib/delete-scores/delete-scores.ts @@ -1,5 +1,6 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; +import { UpdateChartRanking } from "lib/score-import/framework/pb/create-pb-doc"; import { ProcessPBs } from "lib/score-import/framework/pb/process-pbs"; import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/user-game-stats/update-ugs"; import { ScoreDocument, Game, Playtypes, ImportDocument } from "tachi-common"; @@ -65,14 +66,29 @@ export async function DeleteScore(score: ScoreDocument, blacklist = false) { $pull: { scoreIDs: score.scoreID, }, + }, + { + multi: true, } ); } } - // WARN: The array part is important, Set-ing a string explodes - // it into its individual characters. - await ProcessPBs(score.userID, new Set([score.chartID]), logger); + const userHasOtherScores = await db.scores.findOne({ + userID: score.userID, + chart: score.chartID, + }); + + if (userHasOtherScores) { + await ProcessPBs(score.userID, new Set([score.chartID]), logger); + } else { + await db["personal-bests"].remove({ + userID: score.userID, + chartID: score.chartID, + }); + + await UpdateChartRanking(score.chartID); + } await UpdateUsersGamePlaytypeStats(score.game, score.playtype, score.userID, null, logger); @@ -92,101 +108,3 @@ export async function DeleteScore(score: ScoreDocument, blacklist = false) { } } } - -/** - * Deletes multiple provided scores. This is a separate function for - * performance reasons. - * - * This is userID and GPT agnostic. - */ -export async function DeleteMultipleScores(scores: ScoreDocument[], blacklist = false) { - const scoreIDs = scores.map((e) => e.scoreID); - - await db.scores.remove({ - scoreID: { $in: scoreIDs }, - }); - - const sessions = await db.sessions.find({ - "scoreInfo.scoreID": { $in: scoreIDs }, - }); - - // We need to kill sessions that no longer own any scores. - const killSessions = sessions.filter((e) => e.scoreInfo.length === 1); - if (killSessions.length) { - await db.sessions.remove({ - sessionID: { $in: killSessions.map((e) => e.sessionID) }, - }); - } - - const updateSessions = sessions.filter((e) => e.scoreInfo.length > 1); - - if (updateSessions.length) { - await db.sessions.update( - { - sessionID: { $in: updateSessions.map((e) => e.sessionID) }, - }, - { - $pull: { - scoreInfo: { - scoreID: { $in: scoreIDs }, - }, - }, - }, - { - multi: true, - } - ); - } - - const userIDs = DedupeArr(scores.map((e) => e.userID)); - const chartIDs = new Set(scores.map((e) => e.chartID)); - - const pbReprocess = userIDs.map((e) => ProcessPBs(e, chartIDs, logger)); - - await Promise.all(pbReprocess); - - const ugptUpdates: Set = new Set(); - - // This is the easiest way to serialise this. Kinda hacky. - for (const score of scores) { - ugptUpdates.add(`${score.game}-${score.playtype}-${score.userID}`); - } - - const ugptUpdatePromises = [...ugptUpdates].map((e) => { - const [game, playtype, strUserID] = e.split("-") as [Game, Playtypes[Game], string]; - - const userID = Number(strUserID); - - return UpdateUsersGamePlaytypeStats(game, playtype, userID, null, logger); - }); - - await Promise.all(ugptUpdatePromises); - - if (blacklist) { - const alreadyBlacklisted = await db["score-blacklist"].find({ - scoreID: { $in: scoreIDs }, - }); - - let docs = scores.map((e) => ({ userID: e.userID, scoreID: e.scoreID, score: e })); - - if (alreadyBlacklisted.length) { - const xrDiff = new Set(alreadyBlacklisted.map((e) => `${e.userID}-${e.scoreID}`)); - - docs = docs.filter((e) => !xrDiff.has(`${e.userID}-${e.scoreID}`)); - } - - await db["score-blacklist"].insert(docs); - } -} - -/** - * Utility function that gets all of the scores from an import and removes - * those scores. - */ -export async function RevertImport(importDocument: ImportDocument) { - const scores = await db.scores.find({ - scoreID: { $in: importDocument.scoreIDs }, - }); - - await DeleteMultipleScores(scores); -} diff --git a/server/src/lib/score-import/framework/pb/create-pb-doc.ts b/server/src/lib/score-import/framework/pb/create-pb-doc.ts index 3970ccb02..077a77e0b 100644 --- a/server/src/lib/score-import/framework/pb/create-pb-doc.ts +++ b/server/src/lib/score-import/framework/pb/create-pb-doc.ts @@ -24,7 +24,7 @@ export async function CreatePBDoc(userID: integer, chartID: string, logger: KtLo chartID, userID, }); - return; // ?? + return; } const lampPB = (await db.scores.findOne( @@ -89,6 +89,12 @@ export async function UpdateChartRanking(chartID: string) { }); } + // If a score is deleted such that the chart is now empty of + // scores, the below statement will crash with no op specified. + if (bwrite.length === 0) { + return; + } + await db["personal-bests"].bulkWrite(bwrite, { ordered: false }); } diff --git a/server/src/lib/score-import/framework/score-importing/score-importing.ts b/server/src/lib/score-import/framework/score-importing/score-importing.ts index f469dc0ae..88a957f09 100644 --- a/server/src/lib/score-import/framework/score-importing/score-importing.ts +++ b/server/src/lib/score-import/framework/score-importing/score-importing.ts @@ -44,6 +44,16 @@ export async function ImportAllIterableData( context: C, logger: KtLogger ): Promise { + logger.verbose("Getting Blacklist..."); + + // @optimisable: could filter harder with score.game and score.playtype + // stuff. + const blacklist = ( + await db["score-blacklist"].find({ + userID, + }) + ).map((e) => e.scoreID); + logger.verbose(`Starting Data Processing...`); const promises = []; @@ -53,7 +63,15 @@ export async function ImportAllIterableData( // the first set of data. for await (const data of iterableData) { promises.push( - ImportIterableDatapoint(userID, importType, data, ConverterFunction, context, logger) + ImportIterableDatapoint( + userID, + importType, + data, + ConverterFunction, + context, + blacklist, + logger + ) ); } @@ -96,6 +114,7 @@ export async function ImportIterableDatapoint( data: D, ConverterFunction: ConverterFunction, context: C, + blacklist: string[], logger: KtLogger ): Promise { // Converter Function Return @@ -192,12 +211,18 @@ export async function ImportIterableDatapoint( }; } - return ProcessSuccessfulConverterReturn(userID, cfnReturn as ConverterFnSuccessReturn, logger); + return ProcessSuccessfulConverterReturn( + userID, + cfnReturn as ConverterFnSuccessReturn, + blacklist, + logger + ); } export async function ProcessSuccessfulConverterReturn( userID: integer, cfnReturn: ConverterFnSuccessReturn, + blacklist: string[], logger: KtLogger ): Promise { const result = await HydrateAndInsertScore( @@ -205,6 +230,7 @@ export async function ProcessSuccessfulConverterReturn( cfnReturn.dryScore, cfnReturn.chart, cfnReturn.song, + blacklist, logger ); @@ -239,6 +265,7 @@ async function HydrateAndInsertScore( dryScore: DryScore, chart: ChartDocument, song: SongDocument, + blacklist: string[], importLogger: KtLogger ): Promise { const scoreID = CreateScoreID(userID, dryScore, chart.chartID); @@ -246,6 +273,11 @@ async function HydrateAndInsertScore( // sub-context the logger so the below logs are more accurate const logger = AppendLogCtx(scoreID, importLogger); + if (blacklist.length && blacklist.includes(scoreID)) { + logger.verbose("Skipped score, as it was on the blacklist."); + return null; + } + const existingScore = await db.scores.findOne( { scoreID, diff --git a/server/src/server/router/api/v1/scores/_scoreID/router.test.ts b/server/src/server/router/api/v1/scores/_scoreID/router.test.ts index 3bb9a7f96..8fcb08855 100644 --- a/server/src/server/router/api/v1/scores/_scoreID/router.test.ts +++ b/server/src/server/router/api/v1/scores/_scoreID/router.test.ts @@ -175,3 +175,78 @@ t.test("PATCH /api/v1/scores/:scoreID", (t) => { t.end(); }); + +t.test("DELETE /api/v1/scores/:scoreID", (t) => { + t.beforeEach(ResetDBState); + + t.test("Should delete a score if the requester can.", async (t) => { + await db["api-tokens"].insert({ + userID: 1, + identifier: "foo", + permissions: { + delete_score: true, + }, + token: "foo", + }); + + const res = await mockApi + .delete("/api/v1/scores/TESTING_SCORE_ID") + .set("Authorization", "Bearer foo"); + + t.equal(res.statusCode, 200); + + const dbScore = await db.scores.findOne({ scoreID: "TESTING_SCORE_ID" }); + + t.equal(dbScore, null, "Should remove the score from the database."); + + t.end(); + }); + + t.test("Should require authorisation as this user.", async (t) => { + await db["api-tokens"].insert({ + token: "some_dude", + userID: 2, + identifier: "Fake Token", + permissions: { + delete_score: true, + }, + }); + + const res = await mockApi + .delete("/api/v1/scores/TESTING_SCORE_ID") + .set("Authorization", "Bearer some_dude") + .send({ + comment: "foo", + }); + + t.equal(res.statusCode, 403); + + t.match(res.body.description, /You are not authorised/u); + + t.end(); + }); + + t.test("Should require the delete_score permission", async (t) => { + await db["api-tokens"].insert({ + token: "some_token", + userID: 1, + identifier: "another fake token", + permissions: {}, + }); + + const res = await mockApi + .delete("/api/v1/scores/TESTING_SCORE_ID") + .set("Authorization", "Bearer some_token") + .send({ + comment: "foo", + }); + + t.equal(res.statusCode, 403); + + t.match(res.body.description, /delete_score/u); + + t.end(); + }); + + t.end(); +}); diff --git a/server/src/server/router/api/v1/scores/_scoreID/router.ts b/server/src/server/router/api/v1/scores/_scoreID/router.ts index 4dd14edf6..0990481cd 100644 --- a/server/src/server/router/api/v1/scores/_scoreID/router.ts +++ b/server/src/server/router/api/v1/scores/_scoreID/router.ts @@ -7,6 +7,7 @@ import { RequirePermissions } from "server/middleware/auth"; import prValidate from "server/middleware/prudence-validate"; import { GetScoreFromParam, RequireOwnershipOfScore } from "./middleware"; import p from "prudence"; +import { DeleteScore } from "lib/delete-scores/delete-scores"; const router: Router = Router({ mergeParams: true }); @@ -132,4 +133,29 @@ router.patch( } ); +/** + * Deletes the score. + * + * @param blacklist - Whether to blacklist this scoreID or not. + * A blacklisted score will never be reimported. + * + * @name DELETE /api/v1/scores/:scoreID + */ +router.delete( + "/", + RequireOwnershipOfScore, + RequirePermissions("delete_score"), + async (req, res) => { + const score = req[SYMBOL_TachiData]!.scoreDoc!; + + await DeleteScore(score, !!req.body.blacklist); + + return res.status(200).json({ + success: true, + description: `Successfully deleted score.`, + body: {}, + }); + } +); + export default router;