mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-10 08:18:12 +03:00
Merge pull request #605 from TNG-dev/zkldi/issue-604
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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<string, unknown>,
|
||||
headers: FerHeaders,
|
||||
headers: FervidexStaticHeaders,
|
||||
logger: KtLogger
|
||||
): ParserFunctionReturns<FervidexStaticScore, FervidexStaticContext> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,3 +13,8 @@ export interface FervidexStaticScore {
|
||||
export interface FervidexStaticContext {
|
||||
version: GPTSupportedVersions["iidx:DP" | "iidx:SP"];
|
||||
}
|
||||
|
||||
export interface FervidexStaticHeaders {
|
||||
model: string;
|
||||
shouldImportScores: boolean;
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>,
|
||||
headers: FerHeaders,
|
||||
headers: FervidexHeaders,
|
||||
logger: KtLogger
|
||||
): ParserFunctionReturns<FervidexScore, FervidexContext> {
|
||||
const version = SoftwareIDToVersion(headers.model, logger);
|
||||
|
||||
@@ -65,3 +65,7 @@ export interface FervidexContext {
|
||||
version: GPTSupportedVersions["iidx:DP" | "iidx:SP"];
|
||||
timeReceived: number;
|
||||
}
|
||||
|
||||
export interface FerHeaders {
|
||||
model: string;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"chartID": "88eb6cc5683e2740cbd07f588a5f3db1db8d467b",
|
||||
"rgcID": null,
|
||||
"data": {
|
||||
"aiRating": "0",
|
||||
"aiLevel": "0",
|
||||
"notecount": 2256,
|
||||
"hashMD5": "38616b85332037cc12924f2ae2840262",
|
||||
"hashSHA256": "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user