mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-29 18:37:58 +03:00
Merge pull request #336 from TeamNewGuys:zkldi/issue-141-Import-charts-into-the-Database-for-BMS-if-it-isnt-already-present
Define OrphanQueue for TachiIR
This commit is contained in:
+1
-1
@@ -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"
|
||||
},
|
||||
|
||||
Generated
+4
-4
@@ -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
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Vendored
+3
-3
@@ -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<FervidexSettingsDocument>("fer-settings"),
|
||||
"usc-orphan-chart-queue": monkDB.get<USCOrphanChart>("usc-orphan-chart-queue"),
|
||||
"orphan-chart-queue": monkDB.get<OrphanChart>("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}`;
|
||||
|
||||
@@ -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<I extends IDStrings>(
|
||||
idString: I,
|
||||
game: IDStringToGame[I],
|
||||
chartDoc: ChartDocument<I>,
|
||||
songDoc: SongDocument<IDStringToGame[I]>,
|
||||
orphanMatchCriteria: FilterQuery<OrphanChart<I>>,
|
||||
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<I>;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -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<BeatorajaScore, BeatorajaCo
|
||||
throw new InvalidScoreFailure("CN or HCN mode is not supported by this IR.");
|
||||
}
|
||||
|
||||
const chart = (await FindChartOnSHA256("bms", data.sha256)) as ChartDocument<
|
||||
let chart = (await FindChartOnSHA256("bms", data.sha256)) as ChartDocument<
|
||||
"bms:7K" | "bms:14K"
|
||||
> | 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<BeatorajaScore, BeatorajaCo
|
||||
|
||||
return { song, chart, dryScore };
|
||||
};
|
||||
|
||||
function ConvertBeatorajaChartToTachi(chart: BeatorajaChart) {
|
||||
const chartDoc: ChartDocument<"bms:14K" | "bms:7K"> = {
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>,
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
): ParserFunctionReturns<BeatorajaScore, BeatorajaContext> {
|
||||
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[],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
|
||||
@@ -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!);
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -10,5 +10,9 @@
|
||||
{
|
||||
"counterName": "usc-song-id",
|
||||
"value": 2
|
||||
},
|
||||
{
|
||||
"counterName": "bms-song-id",
|
||||
"value": 2
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
Reference in New Issue
Block a user