mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-05 05:18:12 +03:00
Add blacklist support
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user