diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/parser.test.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/parser.test.ts index fcc6d2aec..165f18eea 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/parser.test.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/parser.test.ts @@ -12,7 +12,7 @@ t.test("#ParseFervidexStatic", (t) => { t.test("Should parse static data from body", (t) => { const res = ParseFervidexStatic( FervidexStaticBase, - { model: "LDJ:J:B:A:2020092900" }, + { model: "LDJ:J:B:A:2020092900", shouldImportScores: true }, logger ); @@ -45,12 +45,42 @@ t.test("#ParseFervidexStatic", (t) => { game: "iidx", }); + t.type(res.classHandler, "function", "Should have a function ready for class handling"); + + t.end(); + }); + + t.test("Should not have anything in its iterable if shouldImportScores is false.", (t) => { + const res = ParseFervidexStatic( + FervidexStaticBase, + { model: "LDJ:J:B:A:2020092900", shouldImportScores: false }, + logger + ); + + t.strictSame(res.iterable, []); + + t.hasStrict(res, { + context: { version: "27" }, + game: "iidx", + }); + + t.type( + res.classHandler, + "function", + "Should have a function ready for class handling, regardless of shouldImportScores being false" + ); + t.end(); }); t.test("Should throw an error if no body.scores is present", (t) => { t.throws( - () => ParseFervidexStatic({}, { model: "LDJ:J:B:A:2020092900" }, logger), + () => + ParseFervidexStatic( + {}, + { model: "LDJ:J:B:A:2020092900", shouldImportScores: true }, + logger + ), "Invalid body.scores" ); @@ -62,7 +92,7 @@ t.test("#ParseFervidexStatic", (t) => { () => ParseFervidexStatic( { scores: { nonsenseKey: {} } }, - { model: "LDJ:J:B:A:2020092900" }, + { model: "LDJ:J:B:A:2020092900", shouldImportScores: true }, logger ), "Invalid songID nonsenseKey" @@ -72,7 +102,7 @@ t.test("#ParseFervidexStatic", (t) => { () => ParseFervidexStatic( { scores: { 1000: null } }, - { model: "LDJ:J:B:A:2020092900" }, + { model: "LDJ:J:B:A:2020092900", shouldImportScores: true }, logger ), "Invalid score with songID 1000" @@ -86,7 +116,7 @@ t.test("#ParseFervidexStatic", (t) => { () => ParseFervidexStatic( { scores: { 1000: { spn: null } } }, - { model: "LDJ:J:B:A:2020092900" }, + { model: "LDJ:J:B:A:2020092900", shouldImportScores: true }, logger ), "Invalid score with songID 1000" @@ -96,7 +126,7 @@ t.test("#ParseFervidexStatic", (t) => { () => ParseFervidexStatic( { scores: { 1000: { spn: undefined } } }, - { model: "LDJ:J:B:A:2020092900" }, + { model: "LDJ:J:B:A:2020092900", shouldImportScores: true }, logger ), "Invalid score with songID 1000" @@ -106,7 +136,7 @@ t.test("#ParseFervidexStatic", (t) => { () => ParseFervidexStatic( { scores: { 1000: { spn: "foo" } } }, - { model: "LDJ:J:B:A:2020092900" }, + { model: "LDJ:J:B:A:2020092900", shouldImportScores: true }, logger ), "Invalid score with songID 1000" @@ -124,7 +154,7 @@ t.test("#ParseFervidexStatic", (t) => { 1000: { spn: { ex_score: -1, miss_count: null, clear_type: 0 } }, }, }, - { model: "LDJ:J:B:A:2020092900" }, + { model: "LDJ:J:B:A:2020092900", shouldImportScores: true }, logger ), "Invalid score with songID 1000 at chart spn" @@ -138,7 +168,7 @@ t.test("#ParseFervidexStatic", (t) => { 1000: { spn: { ex_score: 1000, miss_count: "foo", clear_type: 0 } }, }, }, - { model: "LDJ:J:B:A:2020092900" }, + { model: "LDJ:J:B:A:2020092900", shouldImportScores: true }, logger ), "Invalid score with songID 1000 at chart spn" @@ -152,7 +182,7 @@ t.test("#ParseFervidexStatic", (t) => { 1000: { spn: { ex_score: 1000, miss_count: null, clear_type: -1 } }, }, }, - { model: "LDJ:J:B:A:2020092900" }, + { model: "LDJ:J:B:A:2020092900", shouldImportScores: true }, logger ), "Invalid score with songID 1000 at chart spn" @@ -170,7 +200,7 @@ t.test("#ParseFervidexStatic", (t) => { 1000: { spx: { ex_score: 1000, miss_count: null, clear_type: 0 } }, }, }, - { model: "LDJ:J:B:A:2020092900" }, + { model: "LDJ:J:B:A:2020092900", shouldImportScores: true }, logger ), "Invalid chart spx" diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/parser.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/parser.ts index 0865ea4d9..e682a4e8b 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/parser.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/parser.ts @@ -6,8 +6,7 @@ import p from "prudence"; import { IsRecord } from "utils/misc"; import { FormatPrError } from "utils/prudence"; import type { ParserFunctionReturns } from "../../common/types"; -import type { FerHeaders } from "../fervidex/parser"; -import type { FervidexStaticContext, FervidexStaticScore } from "./types"; +import type { FervidexStaticContext, FervidexStaticHeaders, FervidexStaticScore } from "./types"; import type { KtLogger } from "lib/logger/logger"; import type { PrudenceSchema } from "prudence"; @@ -19,10 +18,21 @@ const PR_FERVIDEX_STATIC: PrudenceSchema = { export function ParseFervidexStatic( body: Record, - headers: FerHeaders, + headers: FervidexStaticHeaders, logger: KtLogger ): ParserFunctionReturns { const version = SoftwareIDToVersion(headers.model, logger); + const classHandler = CreateFerStaticClassHandler(body); + + // if we shouldn't import scores, just sync up dans. + if (!headers.shouldImportScores) { + return { + context: { version }, + game: "iidx", + iterable: [], + classHandler, + }; + } const staticScores = body.scores; @@ -80,6 +90,6 @@ export function ParseFervidexStatic( context: { version }, game: "iidx", iterable: scores, - classHandler: CreateFerStaticClassHandler(body), + classHandler, }; } diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/types.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/types.ts index 625940b6e..8c8fbae66 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/types.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/types.ts @@ -13,3 +13,8 @@ export interface FervidexStaticScore { export interface FervidexStaticContext { version: GPTSupportedVersions["iidx:DP" | "iidx:SP"]; } + +export interface FervidexStaticHeaders { + model: string; + shouldImportScores: boolean; +} diff --git a/server/src/lib/score-import/import-types/ir/fervidex/parser.ts b/server/src/lib/score-import/import-types/ir/fervidex/parser.ts index 0866f5c9a..73ccff696 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex/parser.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex/parser.ts @@ -13,7 +13,7 @@ import p from "prudence"; import { ParseEA3SoftID } from "utils/ea3id"; import { FormatPrError, optNull } from "utils/prudence"; import type { ParserFunctionReturns } from "../../common/types"; -import type { FervidexContext, FervidexScore } from "./types"; +import type { FerHeaders as FervidexHeaders, FervidexContext, FervidexScore } from "./types"; import type { KtLogger } from "lib/logger/logger"; import type { PrudenceSchema, ValidSchemaValue } from "prudence"; @@ -108,13 +108,9 @@ export function SoftwareIDToVersion(model: string, logger: KtLogger) { } } -export interface FerHeaders { - model: string; -} - export function ParseFervidexSingle( body: Record, - headers: FerHeaders, + headers: FervidexHeaders, logger: KtLogger ): ParserFunctionReturns { const version = SoftwareIDToVersion(headers.model, logger); diff --git a/server/src/lib/score-import/import-types/ir/fervidex/types.ts b/server/src/lib/score-import/import-types/ir/fervidex/types.ts index 4c2bdf9c2..903e681a1 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex/types.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex/types.ts @@ -65,3 +65,7 @@ export interface FervidexContext { version: GPTSupportedVersions["iidx:DP" | "iidx:SP"]; timeReceived: number; } + +export interface FerHeaders { + model: string; +} diff --git a/server/src/server/router/ir/fervidex/router.test.ts b/server/src/server/router/ir/fervidex/router.test.ts index c54ef1e83..153250b11 100644 --- a/server/src/server/router/ir/fervidex/router.test.ts +++ b/server/src/server/router/ir/fervidex/router.test.ts @@ -490,7 +490,7 @@ t.test("POST /ir/fervidex/profile/submit", (t) => { t.end(); }); - t.test("Should disallow requests from non INF2 if forceStaticImport is false.", async (t) => { + t.test("Should allow requests from non INF2 if forceStaticImport is false.", async (t) => { await db["fer-settings"].update({ userID: 1 }, { $set: { forceStaticImport: false } }); const res = await mockApi @@ -500,7 +500,13 @@ t.test("POST /ir/fervidex/profile/submit", (t) => { .set("X-Software-Model", "LDJ:J:B:A:2020092900") .send(FervidexStaticBase); - t.equal(res.statusCode, 400, "Should be rejected, as FSI is not set."); + t.equal(res.statusCode, 202, "Should be allowed, but no scores should be imported"); + + const scoresNow = await db.scores.count({ + service: "Fervidex Static", + }); + + t.strictSame(scoresNow, 0, "Should have imported no scores"); t.end(); }); diff --git a/server/src/server/router/ir/fervidex/router.ts b/server/src/server/router/ir/fervidex/router.ts index 1d74ffded..cf3c75fdd 100644 --- a/server/src/server/router/ir/fervidex/router.ts +++ b/server/src/server/router/ir/fervidex/router.ts @@ -72,66 +72,6 @@ const ValidateFervidexHeader: RequestHandler = (req, res, next) => { next(); }; -const RequireInf2ModelHeaderOrForceStatic: RequestHandler = async (req, res, next) => { - const settings = await db["fer-settings"].findOne({ - userID: req[SYMBOL_TACHI_API_AUTH].userID!, - }); - - if (settings?.forceStaticImport === true) { - logger.debug(`User ${settings.userID} had forceStaticImport set, allowing request.`); - - // Force static import should ideally only ever be used once. If left on, a users profile - // will get innundated with a bunch of pb imports on every game-load. This is not what - // people want. - // FSI should ideally just be used once to get unreachable scores onto Kamaitachi. Otherwise - // they're doing something wrong. - await db["fer-settings"].update( - { - userID: settings.userID, - }, - { - $set: { - forceStaticImport: false, - }, - } - ); - - next(); - return; - } - - const swModel = req.header("X-Software-Model"); - - if (IsNullishOrEmptyStr(swModel)) { - logger.debug( - `Rejected empty X-Software-Model from user ${req[SYMBOL_TACHI_API_AUTH].userID}.` - ); - return res.status(400).json({ - success: false, - error: `Invalid X-Software-Model.`, - }); - } - - try { - const { model } = ParseEA3SoftID(swModel); - - if (model !== MODEL_INFINITAS_2) { - return res.status(400).json({ - success: false, - error: `Refusing to perform static import from non-infinitas client. To do this anyway, enable Force Static Import.`, - }); - } - } catch (err) { - logger.info(`Invalid softID from ${req[SYMBOL_TACHI_API_AUTH].userID}.`, { err }); - return res.status(400).json({ - success: false, - error: `Invalid X-Software-Model.`, - }); - } - - next(); -}; - const supportedExts = [EXT_HEROIC_VERSE, EXT_BISTROVER]; const ValidateModelHeader: RequestHandler = (req, res, next) => { @@ -219,6 +159,49 @@ router.use( ValidateCards ); +async function ShouldImportScoresFromProfileSubmit(swModel: string, userID: integer) { + const settings = await db["fer-settings"].findOne({ + userID, + }); + + if (settings?.forceStaticImport === true) { + logger.debug(`User ${settings.userID} had forceStaticImport set, allowing request.`); + + // Force static import should ideally only ever be used once. If left on, a users profile + // will get innundated with a bunch of pb imports on every game-load. This is not what + // people want. + // FSI should ideally just be used once to get unreachable scores onto Kamaitachi. Otherwise + // they're doing something wrong. + await db["fer-settings"].update( + { + userID: settings.userID, + }, + { + $set: { + forceStaticImport: false, + }, + } + ); + + return true; + } + + try { + const { model } = ParseEA3SoftID(swModel); + + return model === MODEL_INFINITAS_2; + } catch (err) { + logger.warn( + `Unexpected fail while parsing swModel ${swModel}, has already been validated?.`, + { err } + ); + + // try some good-natured attempt to recover, since this isn't that severe of an + // issue. + return false; + } +} + /** * Submits all of a users data to Tachi. This data is extremely minimal, * as only a users Lamp and Score are sent. As such, this is not the prefered @@ -227,10 +210,18 @@ router.use( * * @name POST /ir/fervidex/profile/submit */ -router.post("/profile/submit", RequireInf2ModelHeaderOrForceStatic, (req, res) => { +router.post("/profile/submit", async (req, res) => { + // guaranteed to exist because of RequireInf2ModelHeader + const model = req.header("X-Software-Model")!; + + const shouldImportScores = await ShouldImportScoresFromProfileSubmit( + model, + req[SYMBOL_TACHI_API_AUTH].userID! + ); + const headers = { - // guaranteed to exist because of RequireInf2ModelHeader - model: req.header("X-Software-Model")!, + model, + shouldImportScores, }; // Perform a fast return here to not allow fervidex to resend requests. diff --git a/server/src/test-utils/mock-db/charts-bms.json b/server/src/test-utils/mock-db/charts-bms.json index 0536b8fa0..a39ae26ec 100644 --- a/server/src/test-utils/mock-db/charts-bms.json +++ b/server/src/test-utils/mock-db/charts-bms.json @@ -4,7 +4,7 @@ "chartID": "88eb6cc5683e2740cbd07f588a5f3db1db8d467b", "rgcID": null, "data": { - "aiRating": "0", + "aiLevel": "0", "notecount": 2256, "hashMD5": "38616b85332037cc12924f2ae2840262", "hashSHA256": "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d", diff --git a/server/src/test-utils/test-data.ts b/server/src/test-utils/test-data.ts index b5244d80e..43cf925aa 100644 --- a/server/src/test-utils/test-data.ts +++ b/server/src/test-utils/test-data.ts @@ -321,7 +321,7 @@ export const BMSGazerChart: ChartDocument<"bms:7K"> = { chartID: "88eb6cc5683e2740cbd07f588a5f3db1db8d467b", rgcID: null, data: { - aiRating: "0", + aiLevel: "0", notecount: 2256, hashMD5: "38616b85332037cc12924f2ae2840262", hashSHA256: "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d",