mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-01 19:38:13 +03:00
Merge pull request #423 from TNG-dev:zkldi/issue-406-Score-Import-Refactor-to-use-separate-process
Refactor Score Import to use workers
This commit is contained in:
Vendored
-1
@@ -52,7 +52,6 @@
|
||||
"ir/fervidex",
|
||||
"ir/fervidex-static",
|
||||
"ir/beatoraja",
|
||||
"ir/chunitachi",
|
||||
"ir/usc",
|
||||
"ir/kshook-sv3c",
|
||||
"api/arc-iidx",
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@
|
||||
"rimraf": "3.0.2",
|
||||
"safe-json-stringify": "1.2.0",
|
||||
"seq-logging": "^1.1.1",
|
||||
"tachi-common": "0.2.42",
|
||||
"tachi-common": "0.2.46",
|
||||
"ts-node": "10.0.0",
|
||||
"typescript": "4.3.4",
|
||||
"winston": "3.3.3",
|
||||
|
||||
Generated
+4
-4
@@ -62,7 +62,7 @@ specifiers:
|
||||
safe-json-stringify: 1.2.0
|
||||
seq-logging: ^1.1.1
|
||||
supertest: 6.1.3
|
||||
tachi-common: 0.2.42
|
||||
tachi-common: 0.2.46
|
||||
tap: 15.0.9
|
||||
ts-node: 10.0.0
|
||||
tsconfig-paths: 3.10.1
|
||||
@@ -106,7 +106,7 @@ dependencies:
|
||||
rimraf: 3.0.2
|
||||
safe-json-stringify: 1.2.0
|
||||
seq-logging: 1.1.1
|
||||
tachi-common: 0.2.42
|
||||
tachi-common: 0.2.46
|
||||
ts-node: 10.0.0_83f53b0a0c5616d3fa00ed4e30b9ce1b
|
||||
typescript: 4.3.4
|
||||
winston: 3.3.3
|
||||
@@ -4176,8 +4176,8 @@ packages:
|
||||
strip-ansi: 6.0.0
|
||||
dev: true
|
||||
|
||||
/tachi-common/0.2.42:
|
||||
resolution: {integrity: sha512-X+oS08NXgVeiPAFiNUzN7DCmiEJor35NCQkh6ia/kcpTAGkRUwst8NFQnHIca2eXDQmndEJg0KGkpYosMs4KYg==}
|
||||
/tachi-common/0.2.46:
|
||||
resolution: {integrity: sha512-BTI3j6joaHLh+T/gapHw5lO9BlB9jSCDtaLIECHkEjY0pUBnXcbcA4GEeMjefXUD65hF04BNkg6J7R6X6Cnkmw==}
|
||||
dependencies:
|
||||
monk: 7.3.4
|
||||
transitivePeerDependencies:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import bunyan from "bunyan";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { Environment, ServerConfig } from "lib/setup/config";
|
||||
import nodemailer, { SentMessageInfo, Transporter } from "nodemailer";
|
||||
import bunyan from "bunyan";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Email HTML is a hellish mess of IE5 era nonsense.
|
||||
// Good luck.
|
||||
|
||||
import { ServerConfig, TachiConfig, TachiServerConfig } from "lib/setup/config";
|
||||
import { ServerConfig, TachiConfig } from "lib/setup/config";
|
||||
|
||||
export function EmailFormatResetPassword(username: string, resetCode: string, ipAddr: string) {
|
||||
return MainHTMLWrapper(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import p from "prudence";
|
||||
import t from "tap";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import p from "prudence";
|
||||
import { ChartDocument, ScoreDocument } from "tachi-common";
|
||||
import t from "tap";
|
||||
import { prAssert } from "test-utils/asserts";
|
||||
import {
|
||||
Testing511SPA,
|
||||
@@ -9,8 +10,7 @@ import {
|
||||
TestingIIDXSPDryScore,
|
||||
TestingSDVXSingleDryScore,
|
||||
} from "test-utils/test-data";
|
||||
import { CreateCalculatedData, CalculateDataForGamePT } from "./calculated-data";
|
||||
import { ChartDocument, ScoreDocument } from "tachi-common";
|
||||
import { CalculateDataForGamePT, CreateCalculatedData } from "./calculated-data";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import {
|
||||
ChartDocument,
|
||||
integer,
|
||||
Grades,
|
||||
Lamps,
|
||||
Game,
|
||||
Playtypes,
|
||||
GetGamePTConfig,
|
||||
Grades,
|
||||
integer,
|
||||
Lamps,
|
||||
Playtypes,
|
||||
} from "tachi-common";
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import { DryScore } from "../common/types";
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import {
|
||||
ParserFunctionReturns,
|
||||
ImportTypeDataMap,
|
||||
ImportTypeContextMap,
|
||||
} from "lib/score-import/import-types/common/types";
|
||||
import { Parsers } from "lib/score-import/import-types/parsers";
|
||||
import { ScoreImportJobData } from "lib/score-import/worker/types";
|
||||
import { ImportTypes } from "tachi-common";
|
||||
|
||||
export function GetInputParser<I extends ImportTypes>(jobData: ScoreImportJobData<I>) {
|
||||
// Retrieve the set parser function for this import type.
|
||||
const ParserFunction = Parsers[jobData.importType];
|
||||
|
||||
const InputParser = (logger: KtLogger) =>
|
||||
// @ts-expect-error TypeScript doesn't like the fact that we
|
||||
// pass this as a rest parameter, since none of the parsers
|
||||
// actually take rest args. However, this is the only way to
|
||||
// achieve the dynamic passing we need to, so lets just override
|
||||
// it here.
|
||||
ParserFunction(...jobData.parserArguments, logger) as Promise<
|
||||
ParserFunctionReturns<ImportTypeDataMap[I], ImportTypeContextMap[I]>
|
||||
>;
|
||||
|
||||
return InputParser;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { PublicUserDocument } from "tachi-common";
|
||||
import t from "tap";
|
||||
import { CreateImportLoggerAndID } from "./import-logger";
|
||||
|
||||
t.test("#CreateImportLoggerAndID", (t) => {
|
||||
const { importID, logger } = CreateImportLoggerAndID(
|
||||
{ username: "foo", id: 1 } as PublicUserDocument,
|
||||
"file/eamusement-iidx-csv"
|
||||
);
|
||||
|
||||
t.match(importID, /^[a-f0-9]{40}$/u, "Should return a 40 character importID.");
|
||||
|
||||
// not possible to automate this, probably.
|
||||
logger.info(
|
||||
"Namespaced log test - if this does not have 'file/eamusement-iidx-csv | foo (#1)' next to it, it's broke."
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -1,13 +1,8 @@
|
||||
import crypto from "crypto";
|
||||
import { KtLogger, rootLogger } from "lib/logger/logger";
|
||||
import { ImportTypes, PublicUserDocument } from "tachi-common";
|
||||
import { Random20Hex } from "utils/misc";
|
||||
import { FormatUserDoc } from "utils/user";
|
||||
|
||||
export function CreateImportLoggerAndID(userDoc: PublicUserDocument, importType: ImportTypes) {
|
||||
const importID = crypto.randomBytes(20).toString("hex");
|
||||
return { logger: CreateScoreLogger(userDoc, importID, importType), importID };
|
||||
}
|
||||
|
||||
export function CreateScoreLogger(
|
||||
user: PublicUserDocument,
|
||||
importID: string,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import {
|
||||
ImportDocument,
|
||||
ImportTypes,
|
||||
PublicUserDocument,
|
||||
integer,
|
||||
SuccessfulAPIResponse,
|
||||
UnsuccessfulAPIResponse,
|
||||
} from "tachi-common";
|
||||
import { ImportInputParser } from "../import-types/common/types";
|
||||
import { CreateImportLoggerAndID } from "./common/import-logger";
|
||||
import ScoreImportMain from "./score-import-main";
|
||||
import { Random20Hex } from "utils/misc";
|
||||
import { ParserArguments } from "../worker/types";
|
||||
import { MakeScoreImport } from "./score-import";
|
||||
import ScoreImportFatalError from "./score-importing/score-import-error";
|
||||
|
||||
export interface WrappedAPIResponse {
|
||||
@@ -15,24 +16,30 @@ export interface WrappedAPIResponse {
|
||||
body: SuccessfulAPIResponse<ImportDocument> | UnsuccessfulAPIResponse;
|
||||
}
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
/**
|
||||
* A thin(ish) wrapper for ScoreImportMain which converts thrown
|
||||
* errors and import documents into a WrappedAPIResponse, which can
|
||||
* be immediately sent with res.json().
|
||||
*/
|
||||
export async function ExpressWrappedScoreImportMain<D, C>(
|
||||
user: PublicUserDocument,
|
||||
export async function ExpressWrappedScoreImportMain<I extends ImportTypes>(
|
||||
userID: integer,
|
||||
userIntent: boolean,
|
||||
importType: ImportTypes,
|
||||
InputParser: ImportInputParser<D, C>
|
||||
importType: I,
|
||||
parserArguments: ParserArguments<I>
|
||||
): Promise<WrappedAPIResponse> {
|
||||
const { importID, logger } = CreateImportLoggerAndID(user, importType);
|
||||
const importID = Random20Hex();
|
||||
|
||||
logger.debug("Received import request.");
|
||||
|
||||
try {
|
||||
const res = await ScoreImportMain(user, userIntent, importType, InputParser, {
|
||||
const res = await MakeScoreImport({
|
||||
importID,
|
||||
logger,
|
||||
importType,
|
||||
userIntent,
|
||||
userID,
|
||||
parserArguments,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -60,8 +67,7 @@ export async function ExpressWrappedScoreImportMain<D, C>(
|
||||
statusCode: 500,
|
||||
body: {
|
||||
success: false,
|
||||
description:
|
||||
"An internal service error has occured. Please do not repeat this request.",
|
||||
description: "An internal service error has occured. This has been reported!",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
KTDataNotFoundFailure,
|
||||
} from "../common/converter-failures";
|
||||
import { ProcessSuccessfulConverterReturn } from "../score-importing/score-importing";
|
||||
import { HandlePostImportSteps } from "../score-import-main";
|
||||
import { HandlePostImportSteps } from "../score-importing/score-import-main";
|
||||
import { GetUserWithID } from "utils/user";
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ServerConfig } from "lib/setup/config";
|
||||
import { ScoreImportJobData } from "../worker/types";
|
||||
import { GetInputParser } from "./common/get-input-parser";
|
||||
import ScoreImportMain from "./score-importing/score-import-main";
|
||||
import { ImportTypes, ImportDocument } from "tachi-common";
|
||||
import ScoreImportQueue from "../worker/queue";
|
||||
|
||||
/**
|
||||
* Makes a score import given ScoreImportJobData.
|
||||
* If USE_EXTERNAL_SCORE_IMPORT_WORKER is set, then this will instead
|
||||
* place this on the score import queue, and the worker will process it.
|
||||
*
|
||||
* Otherwise, it will just perform score importing on the same process.
|
||||
* @returns An import document if awaited, however, you should not
|
||||
* await this if you don't need the import document! Import Documents
|
||||
* may take multiple minutes to generate for large imports. If you control
|
||||
* the client, make it poll /api/v1/ongoing-imports/:importID.
|
||||
*/
|
||||
export async function MakeScoreImport<I extends ImportTypes>(
|
||||
jobData: ScoreImportJobData<I>
|
||||
): Promise<ImportDocument> {
|
||||
if (ServerConfig.USE_EXTERNAL_SCORE_IMPORT_WORKER) {
|
||||
const job = await ScoreImportQueue.add(jobData, {
|
||||
jobId: jobData.importID,
|
||||
});
|
||||
|
||||
return job.finished();
|
||||
} else {
|
||||
const InputParser = GetInputParser(jobData);
|
||||
|
||||
return ScoreImportMain(
|
||||
jobData.userID,
|
||||
jobData.userIntent,
|
||||
jobData.importType,
|
||||
InputParser,
|
||||
jobData.importID
|
||||
);
|
||||
}
|
||||
}
|
||||
+36
-26
@@ -1,40 +1,51 @@
|
||||
import db from "external/mongo/db";
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import {
|
||||
Game,
|
||||
IDStrings,
|
||||
ImportDocument,
|
||||
ImportProcessingInfo,
|
||||
ImportTypes,
|
||||
ImportDocument,
|
||||
PublicUserDocument,
|
||||
Playtypes,
|
||||
Game,
|
||||
integer,
|
||||
IDStrings,
|
||||
Playtypes,
|
||||
PublicUserDocument,
|
||||
} from "tachi-common";
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import { ImportAllIterableData } from "./score-importing/score-importing";
|
||||
import { CreateImportLoggerAndID } from "./common/import-logger";
|
||||
import { CreateSessions } from "./sessions/sessions";
|
||||
import { GetMillisecondsSince } from "utils/misc";
|
||||
import { ProcessPBs } from "./pb/process-pbs";
|
||||
import { UpdateUsersGamePlaytypeStats } from "./user-game-stats/update-ugs";
|
||||
import db from "external/mongo/db";
|
||||
import { GetAndUpdateUsersGoals } from "./goals/goals";
|
||||
import { UpdateUsersMilestones } from "./milestones/milestones";
|
||||
import { ConverterFunction, ImportInputParser } from "../import-types/common/types";
|
||||
import { ScorePlaytypeMap } from "./common/types";
|
||||
import { Converters } from "../import-types/converters";
|
||||
import { ClassHandler } from "./user-game-stats/types";
|
||||
import { GetOrSetUserLock, RemoveUserLock } from "./import-locks/lock";
|
||||
import ScoreImportFatalError from "./score-importing/score-import-error";
|
||||
import { GetUserWithID } from "utils/user";
|
||||
import { ConverterFunction, ImportInputParser } from "../../import-types/common/types";
|
||||
import { Converters } from "../../import-types/converters";
|
||||
import { InternalFailure } from "../common/converter-failures";
|
||||
import { CreateScoreLogger } from "../common/import-logger";
|
||||
import { ScorePlaytypeMap } from "../common/types";
|
||||
import { GetAndUpdateUsersGoals } from "../goals/goals";
|
||||
import { GetOrSetUserLock, RemoveUserLock } from "../import-locks/lock";
|
||||
import { UpdateUsersMilestones } from "../milestones/milestones";
|
||||
import { ProcessPBs } from "../pb/process-pbs";
|
||||
import { CreateSessions } from "../sessions/sessions";
|
||||
import { ClassHandler } from "../user-game-stats/types";
|
||||
import { UpdateUsersGamePlaytypeStats } from "../user-game-stats/update-ugs";
|
||||
import ScoreImportFatalError from "./score-import-error";
|
||||
import { ImportAllIterableData } from "./score-importing";
|
||||
|
||||
/**
|
||||
* Performs a Score Import.
|
||||
*/
|
||||
export default async function ScoreImportMain<D, C>(
|
||||
user: PublicUserDocument,
|
||||
userID: integer,
|
||||
userIntent: boolean,
|
||||
importType: ImportTypes,
|
||||
InputParser: ImportInputParser<D, C>,
|
||||
providedImportObjects?: { logger: KtLogger; importID: string }
|
||||
importID: string,
|
||||
providedLogger?: KtLogger
|
||||
) {
|
||||
const user = await GetUserWithID(userID);
|
||||
|
||||
if (!user) {
|
||||
throw new InternalFailure(
|
||||
`User with ID ${userID} does not exist, but attempted to make an import?`
|
||||
);
|
||||
}
|
||||
|
||||
// in the event of any error, we remove the user lock.
|
||||
try {
|
||||
const lock = await GetOrSetUserLock(user.id);
|
||||
@@ -53,18 +64,17 @@ export default async function ScoreImportMain<D, C>(
|
||||
}
|
||||
|
||||
const timeStarted = Date.now();
|
||||
let importID;
|
||||
let logger;
|
||||
|
||||
if (!providedImportObjects) {
|
||||
if (!providedLogger) {
|
||||
// If they weren't given to us -
|
||||
// we create an "import logger".
|
||||
// this holds a reference to the user's name, ID, and type
|
||||
// of score import for any future debugging.
|
||||
({ importID, logger } = CreateImportLoggerAndID(user, importType));
|
||||
logger = CreateScoreLogger(user, importID, importType);
|
||||
logger.debug("Received import request.");
|
||||
} else {
|
||||
({ importID, logger } = providedImportObjects);
|
||||
logger = providedLogger;
|
||||
}
|
||||
|
||||
// --- 1. Parsing ---
|
||||
@@ -1,11 +1,10 @@
|
||||
import deepmerge from "deepmerge";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { GetKTDataJSON, TestingAlbidaADV, TestingSDVXAlbidaSong } from "test-utils/test-data";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ConvertAPIArcSDVX, ResolveARCSDVXLamp } from "./converter";
|
||||
import { ARCSDVXScore } from "./types";
|
||||
import deepmerge from "deepmerge";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
|
||||
import { agta } from "test-utils/misc";
|
||||
import { MockJSONFetch } from "test-utils/mock-fetch";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ParseArcSDVX } from "./parser";
|
||||
import db from "external/mongo/db";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import { ServerConfig } from "lib/setup/config";
|
||||
import { integer } from "tachi-common";
|
||||
import nodeFetch from "utils/fetch";
|
||||
import { GetArcAuthGuaranteed } from "utils/queries/auth";
|
||||
import { EmptyObject } from "utils/types";
|
||||
import { TraverseKaiAPI } from "../../common/api-kai/traverse-api";
|
||||
import { ParserFunctionReturns } from "../../common/types";
|
||||
import { EmptyObject } from "utils/types";
|
||||
import { ServerConfig } from "lib/setup/config";
|
||||
import { GetArcAuthGuaranteed } from "utils/queries/auth";
|
||||
import { integer } from "tachi-common";
|
||||
|
||||
export async function ParseArcSDVX(
|
||||
userID: integer,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import { ParseKaiIIDX } from "../../common/api-kai/iidx/parser";
|
||||
import { integer } from "tachi-common";
|
||||
import { GetKaiAuthGuaranteed } from "utils/queries/auth";
|
||||
import { ParseKaiIIDX } from "../../common/api-kai/iidx/parser";
|
||||
|
||||
export async function ParseEagIIDX(userID: integer, logger: KtLogger) {
|
||||
const authDoc = await GetKaiAuthGuaranteed(userID, "EAG", logger);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { IIDXDans } from "lib/constants/classes";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
|
||||
import { MockBasicFetch, MockJSONFetch } from "test-utils/mock-fetch";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { KaiTypeToBaseURL } from "../utils";
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { FindIIDXChartOnInGameIDVersion } from "utils/queries/charts";
|
||||
import { KaiContext, KaiIIDXScore } from "../types";
|
||||
import p from "prudence";
|
||||
import { Lamps } from "tachi-common";
|
||||
import { FormatPrError } from "utils/prudence";
|
||||
import { FindIIDXChartOnInGameIDVersion } from "utils/queries/charts";
|
||||
import { FindSongOnID } from "utils/queries/songs";
|
||||
import {
|
||||
InternalFailure,
|
||||
InvalidScoreFailure,
|
||||
KTDataNotFoundFailure,
|
||||
} from "../../../../framework/common/converter-failures";
|
||||
import { FormatPrError } from "utils/prudence";
|
||||
import { FindSongOnID } from "utils/queries/songs";
|
||||
import {
|
||||
GenericGetGradeAndPercent,
|
||||
ParseDateFromString,
|
||||
} from "../../../../framework/common/score-utils";
|
||||
import { Lamps } from "tachi-common";
|
||||
import { ConverterFunction } from "../../types";
|
||||
import { DryScore } from "../../../../framework/common/types";
|
||||
import { ConverterFunction } from "../../types";
|
||||
import { KaiContext, KaiIIDXScore } from "../types";
|
||||
|
||||
const PR_KaiIIDXScore = {
|
||||
music_id: p.isPositiveInteger,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
|
||||
import { MockJSONFetch } from "test-utils/mock-fetch";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ParseKaiIIDX } from "./parser";
|
||||
|
||||
const fakeAuth = {
|
||||
|
||||
@@ -4,7 +4,6 @@ import CreateLogCtx from "lib/logger/logger";
|
||||
import { ServerConfig } from "lib/setup/config";
|
||||
import { KaiAuthDocument } from "tachi-common";
|
||||
import t from "tap";
|
||||
|
||||
import { MockJSONFetch } from "test-utils/mock-fetch";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { CreateKaiReauthFunction } from "./reauth";
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import db from "external/mongo/db";
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error";
|
||||
import { GetKaiTypeClientCredentials, KaiTypeToBaseURL } from "./utils";
|
||||
import nodeFetch from "utils/fetch";
|
||||
import p from "prudence";
|
||||
import { KaiAuthDocument } from "tachi-common";
|
||||
import nodeFetch from "utils/fetch";
|
||||
import { CreateURLWithParams } from "utils/url";
|
||||
import { GetKaiTypeClientCredentials, KaiTypeToBaseURL } from "./utils";
|
||||
|
||||
const REAUTH_SCHEMA = {
|
||||
access_token: "string",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Game, Playtypes, IDStrings, GPTSupportedVersions } from "tachi-common";
|
||||
import { Game, GPTSupportedVersions, IDStrings, Playtypes } from "tachi-common";
|
||||
|
||||
export interface BatchManualContext {
|
||||
game: Game;
|
||||
|
||||
@@ -36,7 +36,6 @@ export interface ImportTypeDataMap {
|
||||
"ir/barbatos": BarbatosScore;
|
||||
"ir/fervidex": FervidexScore;
|
||||
"ir/fervidex-static": FervidexStaticScore;
|
||||
"ir/chunitachi": BatchManualScore;
|
||||
"ir/beatoraja": BeatorajaScore;
|
||||
"ir/usc": USCClientScore;
|
||||
"ir/kshook-sv3c": KsHookSV3CScore;
|
||||
@@ -63,7 +62,6 @@ export interface ImportTypeContextMap {
|
||||
"ir/barbatos": EmptyObject;
|
||||
"ir/fervidex": FervidexContext;
|
||||
"ir/fervidex-static": FervidexStaticContext;
|
||||
"ir/chunitachi": BatchManualContext;
|
||||
"ir/beatoraja": BeatorajaContext;
|
||||
"ir/usc": IRUSCContext;
|
||||
"ir/kshook-sv3c": EmptyObject;
|
||||
@@ -115,3 +113,5 @@ export interface ParserFunctionReturns<D, C> {
|
||||
game: Game;
|
||||
classHandler: ClassHandler | null;
|
||||
}
|
||||
|
||||
export type ParserFunction<D, C, A extends unknown[]> = (...args: A) => ParserFunctionReturns<D, C>;
|
||||
|
||||
@@ -40,6 +40,5 @@ export const Converters: ConverterMap = {
|
||||
"ir/fervidex-static": ConverterIRFervidexStatic,
|
||||
"ir/direct-manual": ConverterBatchManual,
|
||||
"ir/usc": ConverterIRUSC,
|
||||
"ir/chunitachi": ConverterBatchManual,
|
||||
"ir/kshook-sv3c": ConverterIRKsHookSV3C,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import t from "tap";
|
||||
import db from "external/mongo/db";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ConverterIRBarbatos } from "./converter";
|
||||
import deepmerge from "deepmerge";
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { barbScore } from "test-utils/test-data";
|
||||
|
||||
import { ConverterIRBarbatos } from "./converter";
|
||||
import { BarbatosScore } from "./types";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { EmptyObject } from "utils/types";
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import p from "prudence";
|
||||
import ScoreImportFatalError from "../../../framework/score-importing/score-import-error";
|
||||
import { FormatPrError } from "utils/prudence";
|
||||
import { BarbatosScore } from "./types";
|
||||
import { EmptyObject } from "utils/types";
|
||||
import ScoreImportFatalError from "../../../framework/score-importing/score-import-error";
|
||||
import { ParserFunctionReturns } from "../../common/types";
|
||||
import { BarbatosScore } from "./types";
|
||||
|
||||
const PR_Barbatos = {
|
||||
difficulty: p.isIn(1, 2, 3, 4),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import t from "tap";
|
||||
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { Game, Playtypes } from "tachi-common";
|
||||
import t from "tap";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { CreateFerStaticClassHandler } from "./class-handler";
|
||||
import { Playtypes, Game } from "tachi-common";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Lamps } from "tachi-common";
|
||||
import { FindChartOnInGameIDVersion } from "utils/queries/charts";
|
||||
import { FindSongOnID } from "utils/queries/songs";
|
||||
import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils";
|
||||
import {
|
||||
InternalFailure,
|
||||
KTDataNotFoundFailure,
|
||||
} from "../../../framework/common/converter-failures";
|
||||
import { Lamps } from "tachi-common";
|
||||
import { FindChartOnInGameIDVersion } from "utils/queries/charts";
|
||||
import { FervidexStaticContext, FervidexStaticScore } from "./types";
|
||||
import { FERVIDEX_LAMP_LOOKUP, SplitFervidexChartRef } from "../fervidex/converter";
|
||||
import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils";
|
||||
import { DryScore } from "../../../framework/common/types";
|
||||
import { ConverterFunction } from "../../common/types";
|
||||
import { FERVIDEX_LAMP_LOOKUP, SplitFervidexChartRef } from "../fervidex/converter";
|
||||
import { FervidexStaticContext, FervidexStaticScore } from "./types";
|
||||
|
||||
export const ConverterIRFervidexStatic: ConverterFunction<
|
||||
FervidexStaticScore,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { GetKTDataJSON } from "test-utils/test-data";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ParseFervidexStatic } from "./parser";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import p, { PrudenceSchema } from "prudence";
|
||||
import ScoreImportFatalError from "../../../framework/score-importing/score-import-error";
|
||||
import { FormatPrError } from "utils/prudence";
|
||||
import { FervidexStaticContext, FervidexStaticScore } from "./types";
|
||||
import { FerHeaders, SoftwareIDToVersion } from "../fervidex/parser";
|
||||
import { AssertStrAsPositiveInt } from "../../../framework/common/string-asserts";
|
||||
import { CreateFerStaticClassHandler } from "./class-handler";
|
||||
import ScoreImportFatalError from "../../../framework/score-importing/score-import-error";
|
||||
import { ParserFunctionReturns } from "../../common/types";
|
||||
import { FerHeaders, SoftwareIDToVersion } from "../fervidex/parser";
|
||||
import { CreateFerStaticClassHandler } from "./class-handler";
|
||||
import { FervidexStaticContext, FervidexStaticScore } from "./types";
|
||||
|
||||
const PR_FervidexStatic: PrudenceSchema = {
|
||||
ex_score: p.isPositiveInteger,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ParseArcIIDX } from "./api/arc-iidx/parser";
|
||||
import { ParseArcSDVX } from "./api/arc-sdvx/parser";
|
||||
import { ParseEagIIDX } from "./api/eag-iidx/parser";
|
||||
import { ParseEagSDVX } from "./api/eag-sdvx/parser";
|
||||
import { ParseFloIIDX } from "./api/flo-iidx/parser";
|
||||
import { ParseFloSDVX } from "./api/flo-sdvx/parser";
|
||||
import { ParseMinSDVX } from "./api/min-sdvx/parser";
|
||||
import { ParseBatchManualFromObject } from "./common/batch-manual/parser";
|
||||
import ParseBatchManual from "./file/batch-manual/parser";
|
||||
import ParseEamusementIIDXCSV from "./file/eamusement-iidx-csv/parser";
|
||||
import { ParseMerIIDX } from "./file/mer-iidx/parser";
|
||||
import ParsePLIIIDXCSV from "./file/pli-iidx-csv/parser";
|
||||
import { ParseSolidStateXML } from "./file/solid-state-squad/parser";
|
||||
import { ParseBarbatosSingle } from "./ir/barbatos/parser";
|
||||
import { ParseBeatorajaSingle } from "./ir/beatoraja/parser";
|
||||
import ParseDirectManual from "./ir/direct-manual/parser";
|
||||
import { ParseFervidexStatic } from "./ir/fervidex-static/parser";
|
||||
import { ParseFervidexSingle } from "./ir/fervidex/parser";
|
||||
import { ParseKsHookSV3C } from "./ir/kshook-sv3c/parser";
|
||||
import { ParseIRUSC } from "./ir/usc/parser";
|
||||
|
||||
export const Parsers = {
|
||||
"file/solid-state-squad": ParseSolidStateXML,
|
||||
"file/batch-manual": ParseBatchManual,
|
||||
"file/mer-iidx": ParseMerIIDX,
|
||||
"file/pli-iidx-csv": ParsePLIIIDXCSV,
|
||||
"file/eamusement-iidx-csv": ParseEamusementIIDXCSV,
|
||||
|
||||
"api/eag-iidx": ParseEagIIDX,
|
||||
"api/eag-sdvx": ParseEagSDVX,
|
||||
"api/flo-iidx": ParseFloIIDX,
|
||||
"api/flo-sdvx": ParseFloSDVX,
|
||||
"api/min-sdvx": ParseMinSDVX,
|
||||
"api/arc-iidx": ParseArcIIDX,
|
||||
"api/arc-sdvx": ParseArcSDVX,
|
||||
|
||||
"ir/barbatos": ParseBarbatosSingle,
|
||||
"ir/beatoraja": ParseBeatorajaSingle,
|
||||
"ir/fervidex": ParseFervidexSingle,
|
||||
"ir/fervidex-static": ParseFervidexStatic,
|
||||
"ir/direct-manual": ParseDirectManual,
|
||||
"ir/usc": ParseIRUSC,
|
||||
"ir/kshook-sv3c": ParseKsHookSV3C,
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import Queue from "bull";
|
||||
import { RedisClient } from "external/redis/redis";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { Environment, TachiConfig } from "lib/setup/config";
|
||||
import ScoreImportFatalError from "../framework/score-importing/score-import-error";
|
||||
|
||||
const ScoreImportQueue = new Queue(`${TachiConfig.NAME} Score Import Queue`, Environment.redisUrl);
|
||||
|
||||
export default ScoreImportQueue;
|
||||
|
||||
const logger = CreateLogCtx("Score Import Queue");
|
||||
|
||||
// Log errors if and when they occur.
|
||||
ScoreImportQueue.on("failed", (job, err) => {
|
||||
if (err instanceof ScoreImportFatalError) {
|
||||
logger.info(
|
||||
`Job ${job.id} hit ScoreImportFatalError (User Fault) with message: ${err.message}`,
|
||||
err
|
||||
);
|
||||
} else {
|
||||
logger.error(`Job ${job.id} failed unexpectedly with message: ${err.message}`, err);
|
||||
}
|
||||
});
|
||||
|
||||
ScoreImportQueue.on("completed", (job, result) => {
|
||||
logger.debug(`Job ${job.id} finished successfully.`, result);
|
||||
});
|
||||
|
||||
export function CloseScoreImportQueue() {
|
||||
return ScoreImportQueue.close();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Job } from "bull";
|
||||
import { ImportTypes, integer } from "tachi-common";
|
||||
import { Parsers } from "../import-types/parsers";
|
||||
|
||||
// Ok so, please hear me out on this one.
|
||||
// We need a type that removes the last element from an array.
|
||||
// This is because parser functions have variable signatures, but all
|
||||
// end with requiring a logger. We don't want to have to pass the logger,
|
||||
// we just want to pass those other arguments.
|
||||
type RemoveLast<T extends unknown[]> = T extends [...infer RemoveLast, unknown]
|
||||
? RemoveLast
|
||||
: unknown[];
|
||||
|
||||
// Which means we can use generic access to make ParserArguments<"ir/usc">
|
||||
// A type that returns the paramaters of the parser function bound to
|
||||
// "ir/usc".
|
||||
// And then, using RemoveLast, we can remove that logger argument we don't
|
||||
// want to provide.
|
||||
export type ParserArguments<I extends ImportTypes> = RemoveLast<Parameters<typeof Parsers[I]>>;
|
||||
|
||||
// Depending on how you look at it, this is either beautiful
|
||||
// TypeScript power, or brutal TypeScript abuse...
|
||||
|
||||
export interface ScoreImportJobData<I extends ImportTypes> {
|
||||
importType: I;
|
||||
userID: integer;
|
||||
userIntent: boolean;
|
||||
importID: string;
|
||||
|
||||
// ...and well, I'm personally on the fence.
|
||||
|
||||
// Parsers can't have a consistent signature -- they need info
|
||||
// from different sources! As such, we need to pass the parser
|
||||
// arguments in an array, and then it can be respreaded into
|
||||
// the parser by the import code.
|
||||
parserArguments: ParserArguments<I>;
|
||||
}
|
||||
|
||||
export type ScoreImportJob<I extends ImportTypes = ImportTypes> = Job<ScoreImportJobData<I>>;
|
||||
@@ -0,0 +1,69 @@
|
||||
// For scaling performance, running score-importing in a separate worker is preferable
|
||||
// as that way, other API calls don't get halted by particularly expensive imports on
|
||||
// all cores. Parallelism can only get us so far in the same process.
|
||||
|
||||
// You don't have to run this. If it is being ran, you need to set USE_EXTERNAL_SCORE_IMPORT_WORKER
|
||||
// in conf.json5. That will ensure all score import jobs are thrown at redis and eventually
|
||||
// end up here.
|
||||
|
||||
// If you don't, score importing will happen on the same thread as your router. That's probably
|
||||
// fine for lower throughputs, but hey. We're aiming a bit higher.
|
||||
|
||||
// Explicitly set this before importing anything!
|
||||
process.env.IS_SCORE_WORKER_SERVER = "true";
|
||||
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ImportTypes } from "tachi-common";
|
||||
import { FormatUserDoc, GetUserWithID } from "utils/user";
|
||||
import { GetInputParser } from "../framework/common/get-input-parser";
|
||||
import ScoreImportMain from "../framework/score-importing/score-import-main";
|
||||
import ScoreImportQueue from "./queue";
|
||||
import { ScoreImportJob } from "./types";
|
||||
|
||||
const workerLogger = CreateLogCtx(`Import Worker`);
|
||||
|
||||
// Exit if we're not called with node. Think of this like if __name__ != "__main__" in python.
|
||||
if (require.main !== module) {
|
||||
workerLogger.crit(
|
||||
"The Score Import Worker was imported, instead of ran directly with node. This is a fatal error. Exiting."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* When a job is fired, this code will actually process the given data
|
||||
* and import it into the codebase.
|
||||
*/
|
||||
ScoreImportQueue.process(async <I extends ImportTypes>(job: ScoreImportJob<I>) => {
|
||||
const user = await GetUserWithID(job.data.userID);
|
||||
|
||||
if (!user) {
|
||||
workerLogger.severe(
|
||||
`Couldn't find user with ID ${job.data.userID}. Yet a score import from them was made? (Job ID ${job.id}).`
|
||||
);
|
||||
throw new Error(
|
||||
`Couldn't find user with ID ${job.data.userID}. Yet a score import from them was made? (Job ID ${job.id}).`
|
||||
);
|
||||
}
|
||||
|
||||
// Create a logger that we can pass around for context.
|
||||
// This helps us debug what score import did what!
|
||||
const logger = CreateLogCtx(`Score Import ${job.id} ${FormatUserDoc(user)}`);
|
||||
|
||||
const InputParser = GetInputParser(job.data);
|
||||
|
||||
logger.debug(`Starting import.`);
|
||||
|
||||
const importDocument = await ScoreImportMain(
|
||||
user.id,
|
||||
job.data.userIntent,
|
||||
job.data.importType,
|
||||
InputParser,
|
||||
job.data.importID,
|
||||
logger
|
||||
);
|
||||
|
||||
logger.debug(`Finished import.`);
|
||||
|
||||
return importDocument;
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import t from "tap";
|
||||
import db from "external/mongo/db";
|
||||
|
||||
import t from "tap";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { LoadTachiIIDXData } from "test-utils/test-data";
|
||||
import { SearchGameSongs, SearchUsersRegExp } from "./search";
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { ICollection } from "monk";
|
||||
import { FilterQuery } from "mongodb";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import deepmerge from "deepmerge";
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { TachiConfig } from "lib/setup/config";
|
||||
import { FilterQuery } from "mongodb";
|
||||
import { ICollection } from "monk";
|
||||
import {
|
||||
Game,
|
||||
SessionDocument,
|
||||
Playtypes,
|
||||
SongDocument,
|
||||
ChartDocument,
|
||||
Game,
|
||||
integer,
|
||||
Playtypes,
|
||||
PublicUserDocument,
|
||||
SessionDocument,
|
||||
SongDocument,
|
||||
} from "tachi-common";
|
||||
import { EscapeStringRegexp } from "utils/misc";
|
||||
import { GetOnlineCutoff } from "utils/user";
|
||||
import { TachiConfig } from "lib/setup/config";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface TachiServerConfig {
|
||||
RATE_LIMIT: integer;
|
||||
OAUTH_CLIENT_CAP: integer;
|
||||
OPTIONS_ALWAYS_SUCCEEDS?: boolean;
|
||||
USE_EXTERNAL_SCORE_IMPORT_WORKER?: boolean;
|
||||
EMAIL_CONFIG?: {
|
||||
FROM: string;
|
||||
DKIM?: SendMailOptions["dkim"];
|
||||
@@ -125,6 +126,7 @@ const err = p(config, {
|
||||
RATE_LIMIT: p.optional(p.isPositiveInteger),
|
||||
OAUTH_CLIENT_CAP: p.optional(p.isPositiveInteger),
|
||||
OPTIONS_ALWAYS_SUCCEEDS: "*boolean",
|
||||
USE_EXTERNAL_SCORE_IMPORT_WORKER: "*boolean",
|
||||
EMAIL_CONFIG: p.optional({
|
||||
FROM: "string",
|
||||
DKIM: "*object",
|
||||
|
||||
@@ -14,6 +14,7 @@ import { FormatVersion } from "./lib/constants/version";
|
||||
import fetch from "utils/fetch";
|
||||
import http from "http";
|
||||
import { CloseRedisConnection } from "external/redis/redis";
|
||||
import { CloseScoreImportQueue } from "lib/score-import/worker/queue";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -79,6 +80,9 @@ process.on("SIGTERM", () => {
|
||||
logger.info("Closing Redis Connection.", { shutdownInfo: true });
|
||||
CloseRedisConnection();
|
||||
|
||||
logger.info("Closing Score Import Queue.", { shutdownInfo: true });
|
||||
CloseScoreImportQueue();
|
||||
|
||||
logger.info("Everything closed. Waiting for process to exit naturally.", {
|
||||
shutdownInfo: true,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import t from "tap";
|
||||
import { SetRequestPermissions, AllPermissions } from "./auth";
|
||||
import expMiddlewareMock from "express-request-mock";
|
||||
import db from "external/mongo/db";
|
||||
import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi";
|
||||
|
||||
import t from "tap";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { AllPermissions, SetRequestPermissions } from "./auth";
|
||||
|
||||
t.test("#SetRequestPermissions", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
@@ -17,6 +16,7 @@ t.test("#SetRequestPermissions", (t) => {
|
||||
customise_profile: true,
|
||||
},
|
||||
token: "mock_token",
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const { req } = await expMiddlewareMock(SetRequestPermissions, {
|
||||
@@ -32,6 +32,7 @@ t.test("#SetRequestPermissions", (t) => {
|
||||
customise_profile: true,
|
||||
},
|
||||
token: "mock_token",
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
t.end();
|
||||
@@ -45,6 +46,7 @@ t.test("#SetRequestPermissions", (t) => {
|
||||
identifier: "Guest Token",
|
||||
permissions: {},
|
||||
token: null,
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
t.end();
|
||||
@@ -110,6 +112,7 @@ t.test("#SetRequestPermissions", (t) => {
|
||||
identifier: `Session-Key 1`,
|
||||
token: null,
|
||||
permissions: AllPermissions,
|
||||
fromAPIClient: null,
|
||||
});
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ const GuestToken: APITokenDocument = {
|
||||
userID: null,
|
||||
identifier: "Guest Token",
|
||||
permissions: {},
|
||||
fromAPIClient: null,
|
||||
};
|
||||
|
||||
export const AllPermissions: Record<APIPermissions, true> = {
|
||||
@@ -46,6 +47,7 @@ function CreateSetRequestPermissions(errorKeyName: string): RequestHandler {
|
||||
identifier: `Session-Key ${req.session.tachi.user.id}`,
|
||||
token: null,
|
||||
permissions: AllPermissions,
|
||||
fromAPIClient: null,
|
||||
};
|
||||
return next();
|
||||
}
|
||||
@@ -91,6 +93,7 @@ function CreateSetRequestPermissions(errorKeyName: string): RequestHandler {
|
||||
token,
|
||||
permissions: apiTokenData.permissions,
|
||||
identifier: apiTokenData.identifier,
|
||||
fromAPIClient: apiTokenData.fromAPIClient,
|
||||
};
|
||||
|
||||
return next();
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
import { Router } from "express";
|
||||
import { APIImportTypes, FileUploadImportTypes, integer } from "tachi-common";
|
||||
import Prudence from "prudence";
|
||||
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";
|
||||
import { SIXTEEN_MEGABTYES } from "lib/constants/filesize";
|
||||
import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper";
|
||||
import { CreateMulterSingleUploadMiddleware } from "server/middleware/multer-upload";
|
||||
|
||||
import ParseEamusementIIDXCSV from "lib/score-import/import-types/file/eamusement-iidx-csv/parser";
|
||||
import ParseBatchManual from "lib/score-import/import-types/file/batch-manual/parser";
|
||||
import { ParseSolidStateXML } from "lib/score-import/import-types/file/solid-state-squad/parser";
|
||||
import { ParseMerIIDX } from "lib/score-import/import-types/file/mer-iidx/parser";
|
||||
import ParsePLIIIDXCSV from "lib/score-import/import-types/file/pli-iidx-csv/parser";
|
||||
import { TachiConfig } from "lib/setup/config";
|
||||
import { RequirePermissions } from "server/middleware/auth";
|
||||
import { ParseEagIIDX } from "lib/score-import/import-types/api/eag-iidx/parser";
|
||||
import { ParseEagSDVX } from "lib/score-import/import-types/api/eag-sdvx/parser";
|
||||
import { ParseFloIIDX } from "lib/score-import/import-types/api/flo-iidx/parser";
|
||||
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 { SIXTEEN_MEGABTYES } from "lib/constants/filesize";
|
||||
import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper";
|
||||
import { ReprocessOrphan } from "lib/score-import/framework/orphans/orphans";
|
||||
import { MakeScoreImport } from "lib/score-import/framework/score-import";
|
||||
import { ScoreImportJobData } from "lib/score-import/worker/types";
|
||||
import { ServerConfig, TachiConfig } from "lib/setup/config";
|
||||
import Prudence from "prudence";
|
||||
import { RequirePermissions } from "server/middleware/auth";
|
||||
import { CreateMulterSingleUploadMiddleware } from "server/middleware/multer-upload";
|
||||
import prValidate from "server/middleware/prudence-validate";
|
||||
import { APIImportTypes, FileUploadImportTypes } from "tachi-common";
|
||||
import { Random20Hex } from "utils/misc";
|
||||
import { FormatUserDoc, GetUserWithIDGuaranteed } from "utils/user";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -37,9 +27,14 @@ const ParseMultipartScoredata = CreateMulterSingleUploadMiddleware(
|
||||
);
|
||||
|
||||
const fileImportTypes = TachiConfig.IMPORT_TYPES.filter((e) => e.startsWith("file/"));
|
||||
const apiImportTypes = TachiConfig.IMPORT_TYPES.filter((e) => e.startsWith("api/"));
|
||||
|
||||
/**
|
||||
* Import scores from a file. Expects the post request to be multipart, and to provide a scoreData file.
|
||||
*
|
||||
* @param importType - The import type for this file.
|
||||
* @param file - The actual file. Should be passed as multipart.
|
||||
*
|
||||
* @name POST /api/v1/import/file
|
||||
*/
|
||||
router.post(
|
||||
@@ -63,30 +58,45 @@ router.post(
|
||||
|
||||
const importType = req.body.importType as FileUploadImportTypes;
|
||||
|
||||
const inputParser = (logger: KtLogger) =>
|
||||
ResolveFileUploadData(importType, req.file!, req.body, logger);
|
||||
const userIntent = !!req.header("X-User-Intent");
|
||||
|
||||
const userDoc = await GetUserWithIDGuaranteed(req.session.tachi!.user.id);
|
||||
if (ServerConfig.USE_EXTERNAL_SCORE_IMPORT_WORKER) {
|
||||
const importID = Random20Hex();
|
||||
|
||||
// The <any, any> here is deliberate - TS picks the IIDX-CSV generic values
|
||||
// for this function call because it sees them first
|
||||
// but that is ABSOLUTELY not what is actually occuring.
|
||||
// We use this as an override because we know better.
|
||||
// see: https://www.typescriptlang.org/play?ts=4.3.0-beta#code/GYVwdgxgLglg9mABAQQDwBUB8AKYc4BciAYvhpgJSIDeiAsAFCKID0LiAJnAKYDOivKCGDBGAX0aMYYKNwBOwAIYRuJMlhqNmzAEaK5RdOMkNQkWAkQAlbkLlgAynAC23UnGxVqW7W0QAHOVtuMA5EKAALGH5o8IjVIN4QABsoRDhgARdVaX8QNOlBbkUwjMQ5RVCXH2YYTOwAWUVIgDoKqudPRFREAAYWgFYvGu1mILskWj0DRAAiQTlpAHNZxAkmbXXRkfGQexpEaaIARgAmAGY14wZGZNt0vfdEAF5rWz3HbPdPAG4TZGwcEe+AoPyAA
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const responseData = await ExpressWrappedScoreImportMain<any, any>(
|
||||
userDoc,
|
||||
true,
|
||||
importType,
|
||||
inputParser
|
||||
);
|
||||
const job: ScoreImportJobData<FileUploadImportTypes> = {
|
||||
importID,
|
||||
userID: req[SYMBOL_TachiAPIAuth].userID!,
|
||||
userIntent,
|
||||
importType,
|
||||
parserArguments: [req.file, req.body],
|
||||
};
|
||||
|
||||
return res.status(responseData.statusCode).json(responseData.body);
|
||||
// Fire the score import, but make no guarantees about its state.
|
||||
MakeScoreImport<FileUploadImportTypes>(job);
|
||||
|
||||
return res.status(202).json({
|
||||
success: true,
|
||||
description:
|
||||
"Import loaded into queue. You can poll the provided URL for information on when its complete.",
|
||||
body: {
|
||||
url: `${ServerConfig.OUR_URL}/api/v1/imports/${importID}/poll-status`,
|
||||
importID,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Fire the score import and wait for it to finish!
|
||||
const importResponse = await ExpressWrappedScoreImportMain<FileUploadImportTypes>(
|
||||
req[SYMBOL_TachiAPIAuth].userID!,
|
||||
userIntent,
|
||||
importType,
|
||||
[req.file, req.body]
|
||||
);
|
||||
|
||||
return res.status(importResponse.statusCode).json(importResponse.body);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const apiImportTypes = TachiConfig.IMPORT_TYPES.filter((e) => e.startsWith("api/"));
|
||||
|
||||
/**
|
||||
* Import scores from another API. This typically will perform a full sync.
|
||||
* @name POST /api/v1/import/from-api
|
||||
@@ -104,21 +114,44 @@ router.post(
|
||||
async (req, res) => {
|
||||
const importType = req.body.importType as APIImportTypes;
|
||||
|
||||
const userDoc = await GetUserWithIDGuaranteed(req.session.tachi!.user.id);
|
||||
const importID = Random20Hex();
|
||||
|
||||
const inputParser = (logger: KtLogger) =>
|
||||
ResolveAPIImportParser(userDoc.id, importType, logger);
|
||||
const userID = req[SYMBOL_TachiAPIAuth].userID!;
|
||||
|
||||
// see the argument above about typescript falsely expanding types.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const responseData = await ExpressWrappedScoreImportMain<any, any>(
|
||||
userDoc,
|
||||
true,
|
||||
importType,
|
||||
inputParser
|
||||
);
|
||||
const userIntent = !!req.header("X-User-Intent");
|
||||
|
||||
return res.status(responseData.statusCode).json(responseData.body);
|
||||
if (ServerConfig.USE_EXTERNAL_SCORE_IMPORT_WORKER) {
|
||||
const job: ScoreImportJobData<APIImportTypes> = {
|
||||
importID,
|
||||
userID,
|
||||
userIntent,
|
||||
importType,
|
||||
parserArguments: [userID],
|
||||
};
|
||||
|
||||
// Fire the score import, but make no guarantees about its state.
|
||||
MakeScoreImport<APIImportTypes>(job);
|
||||
|
||||
return res.status(202).json({
|
||||
success: true,
|
||||
description:
|
||||
"Import loaded into queue. You can poll the provided URL for information on when its complete.",
|
||||
body: {
|
||||
url: `${ServerConfig.OUR_URL}/api/v1/imports/${importID}/poll-status`,
|
||||
importID,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Fire the score import and wait for it to finish!
|
||||
const importResponse = await ExpressWrappedScoreImportMain<APIImportTypes>(
|
||||
userID,
|
||||
userIntent,
|
||||
importType,
|
||||
[userID]
|
||||
);
|
||||
|
||||
return res.status(importResponse.statusCode).json(importResponse.body);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -178,65 +211,4 @@ router.post("/orphans", RequirePermissions("submit_score"), async (req, res) =>
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Resolves the data from a file upload into an iterable,
|
||||
* The appropriate processing function to map that iterable over,
|
||||
* and and any context the processing may need (such as playtype)
|
||||
*
|
||||
* This also performs validation on the type of file uploaded.
|
||||
* @param importType - The type of import request this was.
|
||||
* @param fileData - The data sent by the user.
|
||||
* @param body - Other data passed by the user in the request body.
|
||||
*/
|
||||
export function ResolveFileUploadData(
|
||||
importType: FileUploadImportTypes,
|
||||
fileData: Express.Multer.File,
|
||||
body: Record<string, unknown>,
|
||||
logger: KtLogger
|
||||
) {
|
||||
switch (importType) {
|
||||
case "file/eamusement-iidx-csv":
|
||||
return ParseEamusementIIDXCSV(fileData, body, logger);
|
||||
case "file/pli-iidx-csv":
|
||||
return ParsePLIIIDXCSV(fileData, body, logger);
|
||||
case "file/batch-manual":
|
||||
return ParseBatchManual(fileData, body, logger);
|
||||
case "file/solid-state-squad":
|
||||
return ParseSolidStateXML(fileData, body, logger);
|
||||
case "file/mer-iidx":
|
||||
return ParseMerIIDX(fileData, body, logger);
|
||||
default:
|
||||
logger.error(
|
||||
`importType ${importType} made it into ResolveFileUploadData, but should have been rejected by Prudence.`
|
||||
);
|
||||
throw new ScoreImportFatalError(400, `Invalid importType of ${importType}.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function ResolveAPIImportParser(
|
||||
userID: integer,
|
||||
importType: APIImportTypes,
|
||||
logger: KtLogger
|
||||
) {
|
||||
switch (importType) {
|
||||
case "api/eag-iidx":
|
||||
return ParseEagIIDX(userID, logger);
|
||||
case "api/eag-sdvx":
|
||||
return ParseEagSDVX(userID, logger);
|
||||
case "api/flo-iidx":
|
||||
return ParseFloIIDX(userID, logger);
|
||||
case "api/flo-sdvx":
|
||||
return ParseFloSDVX(userID, logger);
|
||||
case "api/min-sdvx":
|
||||
return ParseMinSDVX(userID, logger);
|
||||
case "api/arc-iidx":
|
||||
return ParseArcIIDX(userID, logger);
|
||||
case "api/arc-sdvx":
|
||||
return ParseArcSDVX(userID, logger);
|
||||
default:
|
||||
logger.error(`Unknown importType ${importType} has no handler?`);
|
||||
throw new ScoreImportFatalError(500, `Unknown importType ${importType}.`);
|
||||
}
|
||||
}
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error";
|
||||
import ScoreImportQueue from "lib/score-import/worker/queue";
|
||||
import { ServerConfig, TachiConfig } from "lib/setup/config";
|
||||
import { GetRelevantSongsAndCharts } from "utils/db";
|
||||
import { GetUserWithID } from "utils/user";
|
||||
|
||||
@@ -8,6 +11,11 @@ const router: Router = Router({ mergeParams: true });
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
/**
|
||||
* Retrieve an import with this ID.
|
||||
*
|
||||
* @name GET /api/v1/imports/:importID
|
||||
*/
|
||||
router.get("/:importID", async (req, res) => {
|
||||
const importDoc = await db.imports.findOne({
|
||||
importID: req.params.importID,
|
||||
@@ -54,4 +62,83 @@ router.get("/:importID", async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieve the status of an ongoing import.
|
||||
* If the import has been finalised and was successful, return 200.
|
||||
*
|
||||
* If the import is ongoing, return its progress.
|
||||
*
|
||||
* If the import was never ongoing, return 404.
|
||||
*
|
||||
* If the import was finalised and was unsuccessful (i.e. threw a fatal error)
|
||||
* return its error information in expressified form.
|
||||
*
|
||||
* @name GET /api/v1/import/:importID/poll-status
|
||||
*/
|
||||
router.get("/:importID/poll-status", async (req, res) => {
|
||||
if (!ServerConfig.USE_EXTERNAL_SCORE_IMPORT_WORKER) {
|
||||
return res.status(501).json({
|
||||
success: false,
|
||||
description: `${TachiConfig.NAME} does not use an external score import worker. Polling imports is not possible. This import may be ongoing, or it may have never occured.`,
|
||||
});
|
||||
}
|
||||
|
||||
const importDoc = await db.imports.findOne({ importID: req.params.importID });
|
||||
|
||||
if (importDoc) {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Import was completed!`,
|
||||
body: {
|
||||
importStatus: "completed",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const job = await ScoreImportQueue.getJob(req.params.importID);
|
||||
|
||||
if (!job) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
description: `There is no ongoing import here.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (job.isFailed()) {
|
||||
const err = await job.finished();
|
||||
|
||||
if (err instanceof ScoreImportFatalError) {
|
||||
logger.info(err.message);
|
||||
return res.status(err.statusCode).json({
|
||||
success: false,
|
||||
description: err.message,
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
description: `An internal service error has occured with this import. This has been reported!`,
|
||||
});
|
||||
} else if (job.isCompleted()) {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Import was completed!`,
|
||||
body: {
|
||||
importStatus: "completed",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const progress = await job.progress();
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Import is ongoing.`,
|
||||
body: {
|
||||
importStatus: "ongoing",
|
||||
progress,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -134,6 +134,7 @@ t.test("PATCH /api/v1/scores/:scoreID", (t) => {
|
||||
permissions: {
|
||||
customise_score: true,
|
||||
},
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
@@ -156,6 +157,7 @@ t.test("PATCH /api/v1/scores/:scoreID", (t) => {
|
||||
userID: 1,
|
||||
identifier: "another fake token",
|
||||
permissions: {},
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
@@ -186,6 +188,7 @@ t.test("DELETE /api/v1/scores/:scoreID", (t) => {
|
||||
delete_score: true,
|
||||
},
|
||||
token: "foo",
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
@@ -209,6 +212,7 @@ t.test("DELETE /api/v1/scores/:scoreID", (t) => {
|
||||
permissions: {
|
||||
delete_score: true,
|
||||
},
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
@@ -231,6 +235,7 @@ t.test("DELETE /api/v1/scores/:scoreID", (t) => {
|
||||
userID: 1,
|
||||
identifier: "another fake token",
|
||||
permissions: {},
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
|
||||
@@ -161,6 +161,7 @@ t.test("PATCH /api/v1/sessions/:sessionID", (t) => {
|
||||
permissions: {
|
||||
customise_session: true,
|
||||
},
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
@@ -183,6 +184,7 @@ t.test("PATCH /api/v1/sessions/:sessionID", (t) => {
|
||||
userID: 1,
|
||||
identifier: "another fake token",
|
||||
permissions: {},
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
|
||||
@@ -15,18 +15,21 @@ t.test("GET /api/v1/users/:userID/api-tokens", async (t) => {
|
||||
identifier: "foo",
|
||||
permissions: {},
|
||||
token: "tfoo",
|
||||
fromAPIClient: null,
|
||||
},
|
||||
{
|
||||
userID: 1,
|
||||
identifier: "bar",
|
||||
permissions: {},
|
||||
token: "tbar",
|
||||
fromAPIClient: null,
|
||||
},
|
||||
{
|
||||
userID: 2,
|
||||
identifier: "baz",
|
||||
permissions: {},
|
||||
token: "tbaz",
|
||||
fromAPIClient: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -47,12 +50,14 @@ t.test("GET /api/v1/users/:userID/api-tokens", async (t) => {
|
||||
identifier: "foo",
|
||||
permissions: {},
|
||||
token: "tfoo",
|
||||
fromAPIClient: null,
|
||||
},
|
||||
{
|
||||
userID: 1,
|
||||
identifier: "bar",
|
||||
permissions: {},
|
||||
token: "tbar",
|
||||
fromAPIClient: null,
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -117,6 +122,7 @@ t.test("DELETE /api/v1/users/:userID/api-tokens/:token", async (t) => {
|
||||
permissions: {},
|
||||
token: "foo",
|
||||
userID: 2,
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi.delete("/api/v1/users/1/api-tokens/foo").set("Cookie", cookie);
|
||||
@@ -164,7 +170,7 @@ t.test("POST /api/v1/users/:userID/api-tokens/create", async (t) => {
|
||||
identifier: "Hello World",
|
||||
permissions: { submit_score: true, customise_profile: true },
|
||||
userID: 1,
|
||||
fromAPIClient: undefined,
|
||||
fromAPIClient: null,
|
||||
},
|
||||
"Should return a conforming API Token."
|
||||
);
|
||||
@@ -179,7 +185,7 @@ t.test("POST /api/v1/users/:userID/api-tokens/create", async (t) => {
|
||||
identifier: "Hello World",
|
||||
permissions: { submit_score: true, customise_profile: true },
|
||||
userID: 1,
|
||||
fromAPIClient: undefined,
|
||||
fromAPIClient: null,
|
||||
},
|
||||
"Should insert a conforming API Token into the database."
|
||||
);
|
||||
|
||||
@@ -66,7 +66,7 @@ router.post(
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser!;
|
||||
|
||||
let identifier: string;
|
||||
let fromAPIClient;
|
||||
let fromAPIClient = null;
|
||||
|
||||
if (req.body.clientID) {
|
||||
const client = await db["api-clients"].findOne(
|
||||
@@ -131,10 +131,6 @@ router.post(
|
||||
fromAPIClient,
|
||||
};
|
||||
|
||||
// Inserting { fromAPIClient: undefined } results in bson
|
||||
// conversion to {fromAPIClient: null}, which is not the same
|
||||
DeleteUndefinedProps(apiTokenDocument);
|
||||
|
||||
await db["api-tokens"].insert(apiTokenDocument);
|
||||
|
||||
logger.info(`Inserted new API Key for ${FormatUserDoc(user)}.`);
|
||||
|
||||
+5
@@ -43,6 +43,7 @@ t.test("PATCH /api/v1/users/:userID/games/:game/:playtype/settings", (t) => {
|
||||
customise_profile: true,
|
||||
},
|
||||
token: "api_token",
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
@@ -90,6 +91,7 @@ t.test("PATCH /api/v1/users/:userID/games/:game/:playtype/settings", (t) => {
|
||||
customise_profile: true,
|
||||
},
|
||||
token: "api_token",
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
@@ -140,6 +142,7 @@ t.test("PATCH /api/v1/users/:userID/games/:game/:playtype/settings", (t) => {
|
||||
customise_profile: true,
|
||||
},
|
||||
token: "api_token",
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
@@ -159,6 +162,7 @@ t.test("PATCH /api/v1/users/:userID/games/:game/:playtype/settings", (t) => {
|
||||
customise_profile: false,
|
||||
},
|
||||
token: "api_token",
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
@@ -178,6 +182,7 @@ t.test("PATCH /api/v1/users/:userID/games/:game/:playtype/settings", (t) => {
|
||||
customise_profile: true,
|
||||
},
|
||||
token: "api_token",
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
for (const key of [
|
||||
|
||||
+3
@@ -260,6 +260,7 @@ t.test("PUT /api/v1/users/:userID/games/:game/:playtype/showcase", (t) => {
|
||||
customise_profile: true,
|
||||
},
|
||||
token: "alt_token",
|
||||
fromAPIClient: null,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -271,6 +272,7 @@ t.test("PUT /api/v1/users/:userID/games/:game/:playtype/showcase", (t) => {
|
||||
customise_profile: true,
|
||||
},
|
||||
token: "altz_token",
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
@@ -290,6 +292,7 @@ t.test("PUT /api/v1/users/:userID/games/:game/:playtype/showcase", (t) => {
|
||||
customise_profile: false,
|
||||
},
|
||||
token: "unauth_token",
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
|
||||
@@ -39,6 +39,7 @@ t.test("GET /api/v1/users/:userID/integrations/arc", async (t) => {
|
||||
permissions: {},
|
||||
token: "foobar",
|
||||
userID: 1,
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import t from "tap";
|
||||
import db from "external/mongo/db";
|
||||
|
||||
import { UserGameStats } from "tachi-common";
|
||||
import t from "tap";
|
||||
import { CreateFakeAuthCookie } from "test-utils/fake-auth";
|
||||
import mockApi from "test-utils/mock-api";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { UserGameStats } from "tachi-common";
|
||||
import { CreateFakeAuthCookie } from "test-utils/fake-auth";
|
||||
|
||||
t.test("GET /api/v1/users/:userID", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
@@ -86,6 +85,7 @@ t.test("PATCH /api/v1/users/:userID", async (t) => {
|
||||
permissions: {},
|
||||
identifier: "No permissions token",
|
||||
userID: 1,
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res2 = await mockApi.patch("/api/v1/users/1").set("Authorization", "Bearer noperm");
|
||||
|
||||
@@ -144,6 +144,7 @@ t.test("PATCH /api/v1/users/:userID/settings", async (t) => {
|
||||
},
|
||||
token: "no_perm",
|
||||
userID: 1,
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import t from "tap";
|
||||
|
||||
import { CreateFakeAuthCookie, InsertFakeTokenWithAllPerms } from "test-utils/fake-auth";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import mockApi from "test-utils/mock-api";
|
||||
import { TestingBarbatosScore } from "test-utils/test-data";
|
||||
import db from "external/mongo/db";
|
||||
import t from "tap";
|
||||
import { InsertFakeTokenWithAllPerms } from "test-utils/fake-auth";
|
||||
import mockApi from "test-utils/mock-api";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { TestingBarbatosScore } from "test-utils/test-data";
|
||||
|
||||
t.test("POST /ir/barbatos/score/submit", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Router } from "express";
|
||||
import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi";
|
||||
import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper";
|
||||
import { ParseBarbatosSingle } from "lib/score-import/import-types/ir/barbatos/parser";
|
||||
import { RequirePermissions } from "server/middleware/auth";
|
||||
import { GetUserWithIDGuaranteed } from "utils/user";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
@@ -14,13 +12,11 @@ router.use(RequirePermissions("submit_score"));
|
||||
* @name POST /ir/barbatos/score/submit
|
||||
*/
|
||||
router.post("/score/submit", async (req, res) => {
|
||||
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth]!.userID!);
|
||||
|
||||
const responseData = await ExpressWrappedScoreImportMain(
|
||||
userDoc,
|
||||
req[SYMBOL_TachiAPIAuth]!.userID!,
|
||||
false,
|
||||
"ir/barbatos",
|
||||
(logger) => ParseBarbatosSingle(req.body, logger)
|
||||
[req.body]
|
||||
);
|
||||
|
||||
return res.status(responseData.statusCode).json(responseData.body);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import expMiddlewareMock from "express-request-mock";
|
||||
import t from "tap";
|
||||
import { ValidateIRClientVersion } from "./auth";
|
||||
import expMiddlewareMock from "express-request-mock";
|
||||
|
||||
t.test("#ValidateIRClientVersion", (t) => {
|
||||
t.test("Should reject clients that are not supported", async (t) => {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import t from "tap";
|
||||
import deepmerge from "deepmerge";
|
||||
import db from "external/mongo/db";
|
||||
|
||||
import { PublicUserDocument } from "tachi-common";
|
||||
import t from "tap";
|
||||
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);
|
||||
@@ -17,6 +16,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
submit_score: true,
|
||||
},
|
||||
token: "mock_token",
|
||||
fromAPIClient: null,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -142,12 +142,14 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
identifier: "token2",
|
||||
permissions: { submit_score: true },
|
||||
token: "token2",
|
||||
fromAPIClient: null,
|
||||
},
|
||||
{
|
||||
userID: 3,
|
||||
identifier: "token3",
|
||||
permissions: { submit_score: true },
|
||||
token: "token3",
|
||||
fromAPIClient: null,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -320,6 +322,7 @@ t.test("POST /ir/beatoraja/submit-course", (t) => {
|
||||
submit_score: true,
|
||||
},
|
||||
token: "mock_token",
|
||||
fromAPIClient: null,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi";
|
||||
import CreateLogCtx, { KtLogger } from "lib/logger/logger";
|
||||
import { HandleOrphanQueue } from "lib/orphan-queue/orphan-queue";
|
||||
import CreateLogCtx 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 { RequireNotGuest } from "server/middleware/auth";
|
||||
import { UpdateClassIfGreater } from "utils/class";
|
||||
import { GetUserWithIDGuaranteed } from "utils/user";
|
||||
import { ValidateIRClientVersion } from "./auth";
|
||||
import chartsRouter from "./charts/router";
|
||||
|
||||
@@ -24,21 +21,19 @@ router.use(ValidateIRClientVersion);
|
||||
* @name POST /ir/beatoraja/submit-score
|
||||
*/
|
||||
router.post("/submit-score", RequireNotGuest, async (req, res) => {
|
||||
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth]!.userID!);
|
||||
const userID = req[SYMBOL_TachiAPIAuth]!.userID!;
|
||||
|
||||
const ParserFunction = (logger: KtLogger) => ParseBeatorajaSingle(req.body, userDoc.id, logger);
|
||||
|
||||
const importRes = await ExpressWrappedScoreImportMain(
|
||||
userDoc,
|
||||
false,
|
||||
"ir/beatoraja",
|
||||
ParserFunction
|
||||
);
|
||||
const importRes = await ExpressWrappedScoreImportMain(userID, false, "ir/beatoraja", [
|
||||
req.body,
|
||||
userID,
|
||||
]);
|
||||
|
||||
if (!importRes.body.success) {
|
||||
return res.status(400).json(importRes.body);
|
||||
} else if (importRes.body.body.errors.length !== 0) {
|
||||
const type = importRes.body.body.errors[0].type;
|
||||
const errMsg = importRes.body.body.errors[0].message;
|
||||
|
||||
if (type === "KTDataNotFound") {
|
||||
return res.status(202).json({
|
||||
success: true,
|
||||
@@ -47,7 +42,7 @@ router.post("/submit-score", RequireNotGuest, async (req, res) => {
|
||||
} else if (type === "InternalError") {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
description: `[${importRes.body.body.errors[0].type}] - ${importRes.body.body.errors[0].message}`,
|
||||
description: `[${type}] - ${errMsg}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,7 +51,7 @@ router.post("/submit-score", RequireNotGuest, async (req, res) => {
|
||||
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `[${importRes.body.body.errors[0].type}] - ${importRes.body.body.errors[0].message}`,
|
||||
description: `[${type}] - ${errMsg}`,
|
||||
});
|
||||
} else if (importRes.body.body.scoreIDs.length === 0) {
|
||||
return res.status(400).json({
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import t from "tap";
|
||||
import db from "external/mongo/db";
|
||||
|
||||
import { InsertFakeTokenWithAllPerms } from "test-utils/fake-auth";
|
||||
import mockApi from "test-utils/mock-api";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { GetKTDataJSON } from "test-utils/test-data";
|
||||
import deepmerge from "deepmerge";
|
||||
|
||||
t.test("POST /ir/chunitachi/import", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(InsertFakeTokenWithAllPerms("mock_token"));
|
||||
|
||||
const chunitachiBody = GetKTDataJSON("./batch-manual/chunitachi.json");
|
||||
|
||||
t.test("Should work for CHUNITACHI requests", async (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/chunitachi/import")
|
||||
.set("Authorization", `Bearer mock_token`)
|
||||
.send(chunitachiBody);
|
||||
|
||||
t.equal(res.body.success, true, "Should be successful");
|
||||
|
||||
t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores.");
|
||||
|
||||
const scoreCount = await db.scores.count({ service: "ChunItachi" });
|
||||
|
||||
t.equal(scoreCount, 1, "Should import one score.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should reject invalid batch-manual", async (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/chunitachi/import")
|
||||
.set("Authorization", `Bearer mock_token`)
|
||||
.send({});
|
||||
|
||||
t.equal(res.body.success, false, "Should not be successful");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should reject batch-manual requests if game is not chunithm", async (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/chunitachi/import")
|
||||
.set("Authorization", `Bearer mock_token`)
|
||||
.send(deepmerge(chunitachiBody, { meta: { game: "iidx" } }));
|
||||
|
||||
t.equal(res.body.success, false, "Should not be successful");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should reject batch-manual requests if service is not Chunitachi", async (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/chunitachi/import")
|
||||
.set("Authorization", `Bearer mock_token`)
|
||||
.send(deepmerge(chunitachiBody, { meta: { service: "foo bar" } }));
|
||||
|
||||
t.equal(res.body.success, false, "Should not be successful");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should require authentication.", async (t) => {
|
||||
const res = await mockApi.post("/ir/chunitachi/import").send(chunitachiBody);
|
||||
|
||||
t.equal(res.statusCode, 401);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should require a valid auth token.", async (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/chunitachi/import")
|
||||
.set("Authorization", "Bearer invalid_token")
|
||||
.send(chunitachiBody);
|
||||
|
||||
t.equal(res.statusCode, 401);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Router } from "express";
|
||||
import { GetUserWithIDGuaranteed } from "utils/user";
|
||||
import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper";
|
||||
import ParseDirectManual from "lib/score-import/import-types/ir/direct-manual/parser";
|
||||
import { RequirePermissions } from "server/middleware/auth";
|
||||
import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
/**
|
||||
* Submits a single score document from Chunitachi clients.
|
||||
* @name POST /ir/chunitachi/import
|
||||
*/
|
||||
router.post("/import", RequirePermissions("submit_score"), async (req, res) => {
|
||||
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!);
|
||||
|
||||
if (req.body?.meta?.game !== "chunithm") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid Game. Expected 'chunithm', but got ${req.body?.meta?.game}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.meta.service !== "ChunItachi") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Unexpected service ${req.body.head.service} -- expected 'Chunitachi'`,
|
||||
});
|
||||
}
|
||||
|
||||
const responseData = await ExpressWrappedScoreImportMain(
|
||||
userDoc,
|
||||
false,
|
||||
"ir/chunitachi",
|
||||
(logger) => ParseDirectManual(req.body, logger)
|
||||
);
|
||||
|
||||
return res.status(responseData.statusCode).json(responseData.body);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,6 +1,6 @@
|
||||
import t from "tap";
|
||||
import db from "external/mongo/db";
|
||||
|
||||
import { rootLogger } from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
import { CreateFakeAuthCookie } from "test-utils/fake-auth";
|
||||
import mockApi from "test-utils/mock-api";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
@@ -19,6 +19,7 @@ t.test("POST /ir/direct-manual/import", async (t) => {
|
||||
submit_score: false,
|
||||
},
|
||||
userID: 1,
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const res = await mockApi
|
||||
@@ -78,5 +79,64 @@ t.test("POST /ir/direct-manual/import", async (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.beforeEach(() =>
|
||||
db["api-tokens"].insert({
|
||||
token: "mock_token",
|
||||
fromAPIClient: null,
|
||||
identifier: "Mock CHUNITACHI Token",
|
||||
permissions: { submit_score: true },
|
||||
userID: 1,
|
||||
})
|
||||
);
|
||||
|
||||
const chunitachiBody = GetKTDataJSON("./batch-manual/chunitachi.json");
|
||||
|
||||
t.test("Should work for CHUNITACHI requests", async (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/direct-manual/import")
|
||||
.set("Authorization", `Bearer mock_token`)
|
||||
.send(chunitachiBody);
|
||||
|
||||
t.equal(res.body.success, true, "Should be successful");
|
||||
|
||||
t.equal(res.body.body.errors.length, 0, "Should have 0 failed scores.");
|
||||
|
||||
const scoreCount = await db.scores.count({ service: "ChunItachi (DIRECT-MANUAL)" });
|
||||
|
||||
t.equal(scoreCount, 1, "Should import one score.");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should reject invalid batch-manual", async (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/direct-manual/import")
|
||||
.set("Authorization", `Bearer mock_token`)
|
||||
.send({});
|
||||
|
||||
t.equal(res.body.success, false, "Should not be successful");
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should require authentication.", async (t) => {
|
||||
const res = await mockApi.post("/ir/direct-manual/import").send(chunitachiBody);
|
||||
|
||||
t.equal(res.statusCode, 401);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should require a valid auth token.", async (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/direct-manual/import")
|
||||
.set("Authorization", "Bearer invalid_token")
|
||||
.send(chunitachiBody);
|
||||
|
||||
t.equal(res.statusCode, 401);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -12,15 +12,13 @@ const router: Router = Router({ mergeParams: true });
|
||||
* @name POST /ir/direct-manual/import
|
||||
*/
|
||||
router.post("/import", RequirePermissions("submit_score"), async (req, res) => {
|
||||
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!);
|
||||
|
||||
const intent = req.header("X-User-Intent");
|
||||
|
||||
const responseData = await ExpressWrappedScoreImportMain(
|
||||
userDoc,
|
||||
req[SYMBOL_TachiAPIAuth].userID!,
|
||||
!!intent,
|
||||
"ir/direct-manual",
|
||||
(logger) => ParseDirectManual(req.body, logger)
|
||||
[req.body]
|
||||
);
|
||||
|
||||
return res.status(responseData.statusCode).json(responseData.body);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import t from "tap";
|
||||
import db from "external/mongo/db";
|
||||
|
||||
import t from "tap";
|
||||
import { InsertFakeTokenWithAllPerms } from "test-utils/fake-auth";
|
||||
import mockApi from "test-utils/mock-api";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
|
||||
@@ -201,18 +201,16 @@ router.use(
|
||||
* @name POST /ir/fervidex/profile/submit
|
||||
*/
|
||||
router.post("/profile/submit", RequireInf2ModelHeaderOrForceStatic, async (req, res) => {
|
||||
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!);
|
||||
|
||||
const headers = {
|
||||
// guaranteed to exist because of RequireInf2ModelHeader
|
||||
model: req.header("X-Software-Model")!,
|
||||
};
|
||||
|
||||
const responseData = await ExpressWrappedScoreImportMain(
|
||||
userDoc,
|
||||
req[SYMBOL_TachiAPIAuth].userID!,
|
||||
false,
|
||||
"ir/fervidex-static",
|
||||
(logger) => ParseFervidexStatic(req.body, headers, logger)
|
||||
[req.body, headers]
|
||||
);
|
||||
|
||||
if (!responseData.body.success) {
|
||||
@@ -234,8 +232,6 @@ router.post("/profile/submit", RequireInf2ModelHeaderOrForceStatic, async (req,
|
||||
* @name POST /ir/fervidex/score/submit
|
||||
*/
|
||||
router.post("/score/submit", ValidateModelHeader, async (req, res) => {
|
||||
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!);
|
||||
|
||||
const model = req.header("X-Software-Model");
|
||||
|
||||
if (!model) {
|
||||
@@ -250,10 +246,10 @@ router.post("/score/submit", ValidateModelHeader, async (req, res) => {
|
||||
};
|
||||
|
||||
const responseData = await ExpressWrappedScoreImportMain(
|
||||
userDoc,
|
||||
req[SYMBOL_TachiAPIAuth].userID!,
|
||||
true,
|
||||
"ir/fervidex",
|
||||
(logger) => ParseFervidexSingle(req.body, headers, logger)
|
||||
[req.body, headers]
|
||||
);
|
||||
|
||||
if (!responseData.body.success) {
|
||||
|
||||
@@ -81,13 +81,11 @@ router.use(ValidateHeaders);
|
||||
* @name POST /ir/kshook/sv3c/score/save
|
||||
*/
|
||||
router.post("/score/save", async (req, res) => {
|
||||
const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!);
|
||||
|
||||
const responseData = await ExpressWrappedScoreImportMain(
|
||||
userDoc,
|
||||
req[SYMBOL_TachiAPIAuth].userID!,
|
||||
true,
|
||||
"ir/kshook-sv3c",
|
||||
(logger) => ParseKsHookSV3C(req.body, logger)
|
||||
[req.body]
|
||||
);
|
||||
|
||||
if (!responseData.body.success) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Router } from "express";
|
||||
import barbatosIR from "./barbatos/router";
|
||||
import chunitachiIR from "./chunitachi/router";
|
||||
import directManualIR from "./direct-manual/router";
|
||||
import fervidexIR from "./fervidex/router";
|
||||
import uscIR from "./usc/router";
|
||||
@@ -32,7 +31,6 @@ router.use("/beatoraja", SetRequestPermissions, RequireBokutachi, beatorajaIR);
|
||||
// Kamaitachi IRs
|
||||
|
||||
router.use("/barbatos", SetRequestPermissions, RequireNotGuest, RequireKamaitachi, barbatosIR);
|
||||
router.use("/chunitachi", SetRequestPermissions, RequireNotGuest, RequireKamaitachi, chunitachiIR);
|
||||
router.use(
|
||||
"/fervidex",
|
||||
SetFervidexStyleRequestPermissions,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import t from "tap";
|
||||
import deepmerge from "deepmerge";
|
||||
import db from "external/mongo/db";
|
||||
|
||||
import { CDNRetrieve } from "lib/cdn/cdn";
|
||||
import { PBScoreDocument, PublicUserDocument, ScoreDocument } from "tachi-common";
|
||||
import t from "tap";
|
||||
import mockApi from "test-utils/mock-api";
|
||||
import ResetDBState, { ResetCDN } from "test-utils/resets";
|
||||
import deepmerge from "deepmerge";
|
||||
import { PBScoreDocument, ScoreDocument, PublicUserDocument } from "tachi-common";
|
||||
import { GetKTDataBuffer } from "test-utils/test-data";
|
||||
import { CDNRetrieve } from "lib/cdn/cdn";
|
||||
|
||||
async function InsertFakeUSCAuth() {
|
||||
await db["api-tokens"].insert({
|
||||
@@ -16,6 +15,7 @@ async function InsertFakeUSCAuth() {
|
||||
submit_score: true,
|
||||
},
|
||||
token: "foo",
|
||||
fromAPIClient: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ t.test("GET /ir/usc", async (t) => {
|
||||
submit_score: true,
|
||||
},
|
||||
token: "foo",
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
t.beforeEach(ResetDBState);
|
||||
@@ -466,6 +467,7 @@ t.test("POST /scores", (t) => {
|
||||
identifier: "token",
|
||||
permissions: { submit_score: true },
|
||||
token: "token",
|
||||
fromAPIClient: null,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -587,12 +589,14 @@ t.test("POST /scores", (t) => {
|
||||
identifier: "token2",
|
||||
permissions: { submit_score: true },
|
||||
token: "token2",
|
||||
fromAPIClient: null,
|
||||
},
|
||||
{
|
||||
userID: 3,
|
||||
identifier: "token3",
|
||||
permissions: { submit_score: true },
|
||||
token: "token3",
|
||||
fromAPIClient: null,
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
import { Router, RequestHandler } from "express";
|
||||
import p from "prudence";
|
||||
import { SYMBOL_TachiAPIAuth, SYMBOL_TachiData } from "lib/constants/tachi";
|
||||
import { RequestHandler, Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import { CDNStore } from "lib/cdn/cdn";
|
||||
import { GetUSCIRReplayURL } from "lib/cdn/url-format";
|
||||
import { ONE_MEGABYTE } from "lib/constants/filesize";
|
||||
import { SYMBOL_TachiAPIAuth, SYMBOL_TachiData } from "lib/constants/tachi";
|
||||
import { USCIR_MAX_LEADERBOARD_N } from "lib/constants/usc-ir";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { HandleOrphanQueue } from "lib/orphan-queue/orphan-queue";
|
||||
import { AssertStrAsPositiveNonZeroInt } from "lib/score-import/framework/common/string-asserts";
|
||||
import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper";
|
||||
import { ReprocessOrphan } from "lib/score-import/framework/orphans/orphans";
|
||||
import { ServerConfig, TachiConfig } from "lib/setup/config";
|
||||
import p from "prudence";
|
||||
import { RequirePermissions } from "server/middleware/auth";
|
||||
import { CreateMulterSingleUploadMiddleware } from "server/middleware/multer-upload";
|
||||
import {
|
||||
ChartDocument,
|
||||
ImportDocument,
|
||||
PBScoreDocument,
|
||||
SuccessfulAPIResponse,
|
||||
ImportDocument,
|
||||
} from "tachi-common";
|
||||
import { AssertStrAsPositiveNonZeroInt } from "lib/score-import/framework/common/string-asserts";
|
||||
import CreateLogCtx, { KtLogger } from "lib/logger/logger";
|
||||
import { FormatPrError } from "utils/prudence";
|
||||
import { GetBlacklist } from "utils/queries/blacklist";
|
||||
import { AssignToReqTachiData } from "utils/req-tachi-data";
|
||||
import { USCClientChart } from "./types";
|
||||
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";
|
||||
import { USCIR_MAX_LEADERBOARD_N } from "lib/constants/usc-ir";
|
||||
import { CreateMulterSingleUploadMiddleware } from "server/middleware/multer-upload";
|
||||
import { AssignToReqTachiData } from "utils/req-tachi-data";
|
||||
import { CDNStore } from "lib/cdn/cdn";
|
||||
import { ONE_MEGABYTE } from "lib/constants/filesize";
|
||||
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 "lib/orphan-queue/orphan-queue";
|
||||
import { ServerConfig, TachiConfig } from "lib/setup/config";
|
||||
import { ReprocessOrphan } from "lib/score-import/framework/orphans/orphans";
|
||||
import { GetBlacklist } from "utils/queries/blacklist";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -314,25 +312,18 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => {
|
||||
"context.chartHash": chartDoc.data.hashSHA1,
|
||||
});
|
||||
|
||||
for (const score of scoresToDeorphan) {
|
||||
await ReprocessOrphan(score, blacklist, logger);
|
||||
}
|
||||
await Promise.all(
|
||||
scoresToDeorphan.map((score) => ReprocessOrphan(score, blacklist, logger))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const userDoc = await GetUserWithID(req[SYMBOL_TachiAPIAuth]!.userID!);
|
||||
const userID = req[SYMBOL_TachiAPIAuth]!.userID!;
|
||||
|
||||
if (!userDoc) {
|
||||
logger.severe(`User ${req[SYMBOL_TachiAPIAuth]!.userID!} as no parent userDoc?`);
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.SERVER_ERROR,
|
||||
description: "An internal server error has occured.",
|
||||
});
|
||||
}
|
||||
|
||||
const importParser = (logger: KtLogger) => ParseIRUSC(req.body, uscChart.chartHash, logger);
|
||||
|
||||
const importRes = await ExpressWrappedScoreImportMain(userDoc, false, "ir/usc", importParser);
|
||||
const importRes = await ExpressWrappedScoreImportMain(userID, false, "ir/usc", [
|
||||
req.body,
|
||||
uscChart.chartHash,
|
||||
]);
|
||||
|
||||
// If this was an orphan chart request, return ACCEPTED,
|
||||
// since it may be unorphaned in the future
|
||||
@@ -359,11 +350,7 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => {
|
||||
const importDoc = (importRes.body as SuccessfulAPIResponse).body as ImportDocument;
|
||||
|
||||
try {
|
||||
const body = await CreatePOSTScoresResponseBody(
|
||||
userDoc.id,
|
||||
chartDoc,
|
||||
importDoc.scoreIDs[0]
|
||||
);
|
||||
const body = await CreatePOSTScoresResponseBody(userID, chartDoc, importDoc.scoreIDs[0]);
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.SUCCESS,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import t from "tap";
|
||||
import deepmerge from "deepmerge";
|
||||
import db from "external/mongo/db";
|
||||
import { ChartDocument, PBScoreDocument, PublicUserDocument, ScoreDocument } from "tachi-common";
|
||||
import t from "tap";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { CreatePOSTScoresResponseBody, TachiScoreToServerScore } from "./usc";
|
||||
import { ChartDocument, PBScoreDocument, ScoreDocument, PublicUserDocument } from "tachi-common";
|
||||
import deepmerge from "deepmerge";
|
||||
|
||||
const mockScorePB: PBScoreDocument<"usc:Single"> = {
|
||||
chartID: "USC_CHART_ID",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { integer, PBScoreDocument, ScoreDocument, ChartDocument, SongDocument } from "tachi-common";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { GetPBOnChart, GetServerRecordOnChart } from "utils/scores";
|
||||
import { MStoS, Random20Hex } from "utils/misc";
|
||||
import { USCIR_ADJACENT_SCORE_N } from "lib/constants/usc-ir";
|
||||
import db from "external/mongo/db";
|
||||
import { USCIR_ADJACENT_SCORE_N } from "lib/constants/usc-ir";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ChartDocument, integer, PBScoreDocument, ScoreDocument, SongDocument } from "tachi-common";
|
||||
import { MStoS, Random20Hex } from "utils/misc";
|
||||
import { GetPBOnChart, GetServerRecordOnChart } from "utils/scores";
|
||||
import { USCClientChart, USCServerScore } from "./types";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import db from "external/mongo/db";
|
||||
import { APIPermissions } from "tachi-common";
|
||||
import t from "tap";
|
||||
import mockApi from "./mock-api";
|
||||
import { APIPermissions } from "tachi-common";
|
||||
import db from "external/mongo/db";
|
||||
import ResetDBState from "./resets";
|
||||
|
||||
export function RequireAuthPerms(
|
||||
@@ -22,6 +22,7 @@ export function RequireAuthPerms(
|
||||
permissions: {},
|
||||
token: "temp_auth",
|
||||
userID: 1,
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const resAuth = await mockApi[m](url).set("Authorization", "Bearer temp_auth");
|
||||
@@ -35,6 +36,7 @@ export function RequireAuthPerms(
|
||||
permissions: Object.fromEntries(prm.map((e) => [e, true])),
|
||||
token: "temp_auth2",
|
||||
userID: 1,
|
||||
fromAPIClient: null,
|
||||
});
|
||||
|
||||
const resAuthed = await mockApi[m](url).set("Authorization", "Bearer temp_auth2");
|
||||
|
||||
@@ -2,10 +2,12 @@ import { CloseMongoConnection } from "external/mongo/db";
|
||||
import { CloseRedisPubSub } from "external/redis/redis-IPC";
|
||||
import { CloseRedisConnection } from "external/redis/redis";
|
||||
import { CloseServerConnection } from "./mock-api";
|
||||
import { CloseScoreImportQueue } from "lib/score-import/worker/queue";
|
||||
|
||||
export async function CloseAllConnections() {
|
||||
await CloseMongoConnection();
|
||||
await CloseServerConnection();
|
||||
await CloseRedisConnection();
|
||||
await CloseRedisPubSub();
|
||||
await CloseScoreImportQueue();
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export function InsertFakeTokenWithAllPerms(token: string): () => any {
|
||||
identifier: "Mock API Token",
|
||||
permissions: AllPermissions,
|
||||
token,
|
||||
fromAPIClient: null,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import "external/mongo/db";
|
||||
import glob from "glob";
|
||||
import path from "path";
|
||||
import t from "tap";
|
||||
import { CloseAllConnections } from "./close-connections";
|
||||
|
||||
import "external/mongo/db";
|
||||
|
||||
const files = glob.sync(path.join(__dirname, "../../", "**/*.test.ts"));
|
||||
|
||||
process.env.NODE_PATH = path.join(__dirname, "../../");
|
||||
|
||||
Reference in New Issue
Block a user