Add blacklist support

This commit is contained in:
zkldi
2021-10-13 00:16:49 +01:00
parent 3fe34c7758
commit 78c3dc533b
5 changed files with 161 additions and 104 deletions
+19 -101
View File
@@ -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<string> = 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);
}
@@ -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 });
}
@@ -44,6 +44,16 @@ export async function ImportAllIterableData<D, C>(
context: C,
logger: KtLogger
): Promise<ImportProcessingInfo[]> {
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<D, C>(
// 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<D, C>(
data: D,
ConverterFunction: ConverterFunction<D, C>,
context: C,
blacklist: string[],
logger: KtLogger
): Promise<ImportProcessingInfo | null> {
// Converter Function Return
@@ -192,12 +211,18 @@ export async function ImportIterableDatapoint<D, C>(
};
}
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<ImportProcessingInfo | null> {
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<ScoreDocument | null> {
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,
@@ -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();
});
@@ -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;