From a6d23ad40a21d1d0545115a39a9d678909e4eb00 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Wed, 25 May 2022 21:29:54 +0100 Subject: [PATCH] experimental: final cadence validation commit --- server/.eslintignore | 2 - server/.eslintrc | 23 + server/src/datasets/words.ts | 2 +- server/src/external/mongo/db.test.ts | 8 +- server/src/external/mongo/db.ts | 43 +- server/src/external/mongo/indexes.ts | 18 +- server/src/external/redis/redis-IPC.test.ts | 2 +- server/src/external/redis/redis-IPC.ts | 20 +- server/src/external/redis/redis.ts | 6 +- server/src/lib/cdn/cdn.test.ts | 14 +- server/src/lib/constants/classes.ts | 400 ++++++++--------- server/src/lib/constants/err-codes.ts | 10 +- server/src/lib/constants/version.ts | 44 +- server/src/lib/database-seeds/repo.ts | 2 +- server/src/lib/email/client.ts | 2 + .../create-game-settings.test.ts | 2 +- server/src/lib/handlers/sigterm.ts | 10 +- server/src/lib/imports/imports.test.ts | 5 +- server/src/lib/imports/imports.ts | 2 +- server/src/lib/jobs/backsync-bms-pms-data.ts | 8 +- server/src/lib/jobs/bms-table-sync.ts | 16 +- server/src/lib/jobs/deorphan-scores.ts | 8 +- .../lib/jobs/inline-job-runner/job-runner.ts | 14 +- server/src/lib/jobs/ugs-snapshot.ts | 17 +- server/src/lib/jobs/update-dp-tiers.ts | 14 +- server/src/lib/logger/colours.ts | 19 +- server/src/lib/logger/discord-transport.ts | 39 +- server/src/lib/logger/logger.test.ts | 8 +- server/src/lib/logger/logger.ts | 22 +- server/src/lib/migration/migrations.test.ts | 10 +- server/src/lib/migration/migrations.ts | 4 +- .../lib/notifications/notifications.test.ts | 2 +- .../calculated-data/calculated-data.test.ts | 29 +- .../calculated-data/calculated-data.ts | 18 +- .../framework/calculated-data/stats.test.ts | 34 +- .../framework/calculated-data/stats.ts | 26 +- .../framework/common/get-input-parser.ts | 1 + .../framework/common/score-utils.test.ts | 53 ++- .../framework/common/string-asserts.test.ts | 13 +- .../score-import/framework/common/types.ts | 3 +- .../framework/goals/goals.test.ts | 39 +- .../lib/score-import/framework/goals/goals.ts | 6 +- .../framework/milestones/milestones.test.ts | 9 +- .../framework/milestones/milestones.ts | 7 +- .../framework/orphans/orphans.test.ts | 20 +- .../score-import/framework/orphans/orphans.ts | 4 +- .../framework/pb/create-pb-doc.test.ts | 6 +- .../framework/pb/create-pb-doc.ts | 38 +- .../framework/pb/game-specific-merge.ts | 9 +- .../framework/pb/process-pbs.test.ts | 6 +- .../score-import/framework/score-import.ts | 6 +- .../score-importing/insert-score.test.ts | 17 +- .../framework/score-importing/insert-score.ts | 5 +- .../score-importing/score-id.test.ts | 5 +- .../score-import-error.test.ts | 2 +- .../score-importing/score-import-main.ts | 2 +- .../score-importing/score-importing.ts | 4 +- .../sessions/calculated-data.test.ts | 6 +- .../framework/sessions/calculated-data.ts | 182 +++----- .../sessions/name-generation.test.ts | 2 +- .../framework/sessions/sessions.test.ts | 18 +- .../builtin-class-handlers.test.ts | 6 +- .../framework/user-game-stats/classes.test.ts | 9 +- .../framework/user-game-stats/classes.ts | 7 +- .../framework/user-game-stats/rating.test.ts | 2 +- .../framework/user-game-stats/rating.ts | 188 ++++---- .../user-game-stats/update-ugs.test.ts | 8 +- .../api/arc-iidx/class-handler.test.ts | 5 +- .../api/arc-iidx/class-handler.ts | 31 +- .../api/arc-iidx/converter.test.ts | 4 +- .../import-types/api/arc-iidx/parser.test.ts | 2 +- .../api/arc-sdvx/converter.test.ts | 4 +- .../import-types/api/arc-sdvx/converter.ts | 4 +- .../import-types/api/arc-sdvx/parser.test.ts | 2 +- .../common/api-kai/iidx/class-handler.test.ts | 4 +- .../common/api-kai/iidx/class-handler.ts | 32 +- .../common/api-kai/iidx/converter.test.ts | 3 +- .../common/api-kai/iidx/converter.ts | 6 +- .../common/api-kai/iidx/parser.test.ts | 2 +- .../common/api-kai/reauth.test.ts | 4 +- .../import-types/common/api-kai/reauth.ts | 14 +- .../common/api-kai/sdvx/class-handler.test.ts | 4 +- .../common/api-kai/sdvx/class-handler.ts | 23 +- .../common/api-kai/sdvx/converter.test.ts | 8 +- .../common/api-kai/sdvx/converter.ts | 4 +- .../common/api-kai/sdvx/parser.test.ts | 2 +- .../common/api-kai/traverse-api.test.ts | 10 +- .../common/api-kai/traverse-api.ts | 25 +- .../import-types/common/api-kai/types.ts | 9 +- .../import-types/common/api-kai/utils.ts | 35 +- .../common/batch-manual/converter.test.ts | 27 +- .../common/batch-manual/converter.ts | 423 ++++++++++-------- .../common/batch-manual/parser.test.ts | 12 +- .../common/batch-manual/parser.ts | 75 ++-- .../common/eamusement-iidx-csv/converter.ts | 4 +- .../common/eamusement-iidx-csv/parser.test.ts | 4 +- .../common/eamusement-iidx-csv/parser.ts | 70 +-- .../import-types/file/batch-manual/parser.ts | 5 +- .../eamusement-sdvx-csv/converter.test.ts | 17 +- .../file/eamusement-sdvx-csv/parser.test.ts | 7 +- .../file/mer-iidx/converter.test.ts | 23 +- .../import-types/file/mer-iidx/converter.ts | 2 +- .../import-types/file/mer-iidx/parser.test.ts | 41 +- .../import-types/file/mer-iidx/parser.ts | 6 +- .../file/solid-state-squad/converter.test.ts | 7 +- .../file/solid-state-squad/converter.ts | 69 +-- .../file/solid-state-squad/parser.test.ts | 2 +- .../file/solid-state-squad/parser.ts | 34 +- .../ir/barbatos/converter.test.ts | 11 +- .../import-types/ir/barbatos/converter.ts | 4 +- .../import-types/ir/barbatos/parser.test.ts | 7 +- .../import-types/ir/barbatos/parser.ts | 6 +- .../import-types/ir/beatoraja/converter.ts | 4 +- .../import-types/ir/beatoraja/parser.ts | 10 +- .../import-types/ir/beatoraja/types.ts | 4 +- .../ir/fervidex-static/class-handler.test.ts | 4 +- .../ir/fervidex-static/parser.test.ts | 8 +- .../import-types/ir/fervidex-static/parser.ts | 37 +- .../ir/fervidex/converter.test.ts | 79 ++-- .../import-types/ir/fervidex/parser.test.ts | 2 +- .../import-types/ir/fervidex/parser.ts | 4 +- .../import-types/ir/fervidex/types.ts | 4 +- .../ir/kshook-sv6c/converter.test.ts | 4 +- .../ir/kshook-sv6c/parser.test.ts | 2 +- .../import-types/ir/kshook-sv6c/parser.ts | 6 +- .../import-types/ir/lr2hook/converter.test.ts | 4 +- .../import-types/ir/lr2hook/parser.test.ts | 4 +- .../import-types/ir/lr2hook/parser.ts | 6 +- .../import-types/ir/usc/converter.test.ts | 8 +- .../import-types/ir/usc/converter.ts | 31 +- .../import-types/ir/usc/parser.test.ts | 4 +- .../import-types/ir/usc/parser.ts | 6 +- server/src/lib/score-import/worker/types.ts | 15 +- server/src/lib/score-import/worker/worker.ts | 14 +- .../lib/score-mutation/delete-scores.test.ts | 4 +- .../lib/score-mutation/update-score.test.ts | 4 +- server/src/lib/search/search.test.ts | 26 +- server/src/lib/search/search.ts | 58 +-- server/src/lib/showcase/evaluator.test.ts | 13 +- server/src/lib/showcase/evaluator.ts | 16 +- server/src/lib/showcase/get-related.ts | 47 +- server/src/lib/showcase/get-stats.test.ts | 12 +- server/src/lib/showcase/showcase.test.ts | 2 +- server/src/lib/targets/goal-utils.ts | 62 ++- server/src/lib/targets/goals.test.ts | 4 +- server/src/lib/targets/goals.ts | 267 +++++------ server/src/lib/targets/milestones.ts | 34 +- server/src/lib/webhooks/webhooks.ts | 2 +- server/src/main.ts | 4 +- server/src/scripts/clone-and-anonymise-db.ts | 19 +- server/src/scripts/email/send-test-email.ts | 11 +- server/src/scripts/folders/folder-cache.ts | 10 +- .../scripts/iidx-tierlist/get-sp12-data.ts | 4 - server/src/scripts/init-sequence-docs.ts | 10 +- .../src/scripts/invites/create-invite-code.ts | 12 +- .../patches/patch-invalid-iidx-grades.ts | 6 +- server/src/scripts/set-indexes.ts | 14 +- .../scripts/state-sync/dedupe-score-ids.ts | 46 +- .../scripts/state-sync/recalc-all-scores.ts | 13 +- .../state-sync/recalc-game-profiles.ts | 37 +- .../src/scripts/state-sync/recalc-sessions.ts | 10 +- server/src/scripts/state-sync/sync-state.ts | 9 +- .../src/scripts/state-sync/update-all-pbs.ts | 13 +- server/src/scripts/sync-database.ts | 6 +- server/src/scripts/validate-database.ts | 49 +- server/src/server/middleware/auth.test.ts | 5 +- server/src/server/middleware/auth.ts | 8 +- server/src/server/middleware/multer-upload.ts | 2 +- .../middleware/prudence-validate.test.ts | 7 +- .../server/middleware/prudence-validate.ts | 6 +- server/src/server/middleware/rate-limiter.ts | 6 +- .../src/server/middleware/request-logger.ts | 3 + .../middleware/sanitise-qstring.test.ts | 2 +- .../src/server/middleware/sanitise-qstring.ts | 6 +- .../src/server/middleware/update-last-seen.ts | 4 +- .../server/router/api/v1/admin/router.test.ts | 5 +- .../server/router/api/v1/auth/auth.test.ts | 5 +- server/src/server/router/api/v1/auth/auth.ts | 13 - .../server/router/api/v1/auth/router.test.ts | 14 +- .../router/api/v1/clients/middleware.test.ts | 7 +- .../router/api/v1/clients/router.test.ts | 7 +- .../_playtype/charts/_chartID/middleware.ts | 4 +- .../_playtype/charts/_chartID/router.test.ts | 4 +- .../_game/_playtype/charts/_chartID/router.ts | 2 +- .../_game/_playtype/charts/router.test.ts | 12 +- .../v1/games/_game/_playtype/charts/router.ts | 4 +- .../_game/_playtype/folders/router.test.ts | 2 +- .../games/_game/_playtype/folders/router.ts | 7 +- .../v1/games/_game/_playtype/router.test.ts | 5 +- .../api/v1/games/_game/_playtype/router.ts | 16 +- .../v1/games/_game/_playtype/scores/router.ts | 3 +- .../_game/_playtype/tables/router.test.ts | 3 +- .../v1/games/_game/_playtype/tables/router.ts | 2 +- .../_playtype/targets/goals/router.test.ts | 16 +- .../targets/milestone-sets/router.test.ts | 8 +- .../targets/milestones/router.test.ts | 32 +- .../_game/_playtype/targets/router.test.ts | 15 +- .../src/server/router/api/v1/import/router.ts | 2 +- .../router/api/v1/imports/router.test.ts | 5 + .../server/router/api/v1/imports/router.ts | 5 +- .../api/v1/scores/_scoreID/router.test.ts | 2 + .../router/api/v1/scores/_scoreID/router.ts | 2 +- .../router/api/v1/search/router.test.ts | 6 +- .../api/v1/sessions/_sessionID/router.test.ts | 3 +- .../api/v1/sessions/_sessionID/router.ts | 1 - .../router/api/v1/status/router.test.ts | 2 +- .../users/_userID/api-tokens/router.test.ts | 2 +- .../api/v1/users/_userID/banner/router.ts | 1 - .../folders/_folderID/router.test.ts | 23 +- .../_playtype/folders/_folderID/router.ts | 53 ++- .../games/_game/_playtype/folders/router.ts | 7 +- .../games/_game/_playtype/pbs/router.test.ts | 20 +- .../_game/_playtype/rivals/router.test.ts | 2 +- .../games/_game/_playtype/router.test.ts | 10 +- .../_game/_playtype/scores/router.test.ts | 27 +- .../_game/_playtype/sessions/router.test.ts | 15 +- .../_game/_playtype/settings/router.test.ts | 4 +- .../_game/_playtype/showcase/router.test.ts | 5 +- .../_game/_playtype/tables/router.test.ts | 3 +- .../_playtype/targets/goals/router.test.ts | 35 +- .../targets/milestones/router.test.ts | 10 +- .../_game/_playtype/targets/router.test.ts | 12 +- .../v1/users/_userID/imports/router.test.ts | 2 +- .../integrations/fervidex/router.test.ts | 2 +- .../kai/_kaiType/middleware.test.ts | 2 +- .../integrations/kai/_kaiType/router.ts | 1 - .../v1/users/_userID/invites/router.test.ts | 5 +- .../api/v1/users/_userID/invites/router.ts | 1 - .../router/api/v1/users/_userID/middleware.ts | 3 +- .../_userID/notifications/router.test.ts | 2 +- .../v1/users/_userID/notifications/router.ts | 1 - .../api/v1/users/_userID/pfp/router.test.ts | 1 + .../router/api/v1/users/_userID/pfp/router.ts | 1 - .../api/v1/users/_userID/router.test.ts | 6 +- .../v1/users/_userID/settings/router.test.ts | 7 +- .../server/router/api/v1/users/router.test.ts | 4 +- .../server/router/ir/beatoraja/auth.test.ts | 2 +- .../_chartSHA256/convert-scores.test.ts | 4 +- .../charts/_chartSHA256/router.test.ts | 5 +- .../beatoraja/charts/_chartSHA256/router.ts | 24 +- .../server/router/ir/beatoraja/router.test.ts | 4 +- .../router/ir/direct-manual/router.test.ts | 12 +- .../server/router/ir/fervidex/router.test.ts | 10 + .../server/router/ir/lr2hook/router.test.ts | 2 +- server/src/server/router/ir/lr2hook/router.ts | 2 +- .../router/ir/usc/_playtype/router.test.ts | 14 +- .../router/ir/usc/_playtype/usc.test.ts | 19 +- .../src/utils/calculations/recalc-scores.ts | 1 - server/src/utils/crypto.test.ts | 2 +- server/src/utils/db.test.ts | 2 +- server/src/utils/folder.test.ts | 13 +- server/src/utils/misc.test.ts | 4 +- server/src/utils/naive-csv-parser.test.ts | 4 +- server/src/utils/prudence.ts | 4 +- server/src/utils/session.test.ts | 2 +- server/src/utils/user.test.ts | 6 +- 256 files changed, 2492 insertions(+), 1937 deletions(-) diff --git a/server/.eslintignore b/server/.eslintignore index c53500a73..64314f3ee 100644 --- a/server/.eslintignore +++ b/server/.eslintignore @@ -5,5 +5,3 @@ node_modules js - -**/*.test.ts \ No newline at end of file diff --git a/server/.eslintrc b/server/.eslintrc index 25583d204..c96ed3dd2 100644 --- a/server/.eslintrc +++ b/server/.eslintrc @@ -6,5 +6,28 @@ ], "extends": [ "plugin:cadence/recommended" + ], + "rules": { + "@typescript-eslint/no-non-null-assertion": "off" + }, + "overrides": [ + { + "files": [ + "**/*.test.ts" + ], + "rules": { + // Our test files break these rules *all* the time, and there's + // no point refactoring. Our tests deliberately play with the + // dynamic nature of TS to more accurately test arbitrary input. + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-call": "off", + // we abuse this one for faking data, but we really shouldn't. + "@typescript-eslint/consistent-type-assertions": "warn" + } + } ] } \ No newline at end of file diff --git a/server/src/datasets/words.ts b/server/src/datasets/words.ts index 8aaafb7d8..84a4cf042 100644 --- a/server/src/datasets/words.ts +++ b/server/src/datasets/words.ts @@ -2033,7 +2033,7 @@ export const nouns = [ "Bit", // "Black", I have visions of this one resulting in racism - "Bottom", // This one gets to stay because it's hilarious + "Bottom", "Choice", "Gift", "Impact", diff --git a/server/src/external/mongo/db.test.ts b/server/src/external/mongo/db.test.ts index 13a8e5e19..fc126e18a 100644 --- a/server/src/external/mongo/db.test.ts +++ b/server/src/external/mongo/db.test.ts @@ -1,26 +1,32 @@ +import db from "./db"; import t from "tap"; import ResetDBState from "test-utils/resets"; -import db from "./db"; t.test("ID field autoprojection", async (t) => { await ResetDBState(); const res = await db.scores.findOne(); + t.equal(res!._id, undefined); const res2 = await db.scores.findOne({}, { projectID: true }); + t.not(res2!._id, undefined); const res3 = await db.scores.findOne({}, { projection: { scoreID: 1 } }); + t.equal(res3!._id, undefined); const res4 = await db.scores.findOne({}, { projection: { scoreID: 1 }, projectID: true }); + t.not(res4!._id, undefined); const res5 = await db.scores.findOne({}, { projection: { scoreID: 0 } }); + t.equal(res5!._id, undefined); const res6 = await db.scores.findOne({}, { projection: { scoreID: 0 }, projectID: true }); + t.not(res6!._id, undefined); t.end(); diff --git a/server/src/external/mongo/db.ts b/server/src/external/mongo/db.ts index 7c359856c..4826ef2b2 100644 --- a/server/src/external/mongo/db.ts +++ b/server/src/external/mongo/db.ts @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/strict-boolean-expressions */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +// ^ These rules are disabled for good reason. We have to deal with some very nonsensical types here +// so we just disable these rules. I know, it sucks, but we'll live. import { ONE_MINUTE } from "lib/constants/time"; import CreateLogCtx from "lib/logger/logger"; import { Environment, ServerConfig } from "lib/setup/config"; @@ -74,25 +78,25 @@ monkDB process.exit(1); }); -const RemoveIDMiddleware: TMiddleware = - () => - (next) => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (args: any, method) => { - if ((method === "find" || method === "findOne") && !args.options.projectID) { - if (args.options.projection) { - args.options.projection._id = 0; - } else { - args.options.projection = { _id: 0 }; - } +/** + * Removes _id from the returns of find/findOne. Note that this function is littered with eslint-disables + * due to it working with some pretty wobbly types. + */ +const RemoveIDFromFindReturnsMiddleware: TMiddleware = () => (next) => (args: any, method) => { + if ((method === "find" || method === "findOne") && !args.options.projectID) { + if (args.options.projection) { + args.options.projection._id = 0; + } else { + args.options.projection = { _id: 0 }; } + } - return next(args, method); - }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return next(args, method); +}; // a bug in monks types means that :any has to be used here. Maybe we'll make a PR for this? -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const StripIDMiddleware: TMiddleware = () => (next) => (args: any, method) => { +const StripIDFromDBInsertsMiddleware: TMiddleware = () => (next) => (args: any, method) => { if (method === "insert") { if (Array.isArray(args.data)) { for (const d of args.data) { @@ -103,11 +107,12 @@ const StripIDMiddleware: TMiddleware = () => (next) => (args: any, method) => { } } + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument return next(args, method); }; -monkDB.addMiddleware(StripIDMiddleware); -monkDB.addMiddleware(RemoveIDMiddleware); +monkDB.addMiddleware(StripIDFromDBInsertsMiddleware); +monkDB.addMiddleware(RemoveIDFromFindReturnsMiddleware); export async function CloseMongoConnection() { await monkDB.close(); @@ -178,9 +183,11 @@ const db = { "user-settings": monkDB.get("user-settings"), "user-private-information": monkDB.get("user-private-information"), "api-clients": monkDB.get("api-clients"), + + // i've inlined this one because i don't see it appearing anywhere else. "oauth2-auth-codes": - // i've inlined this one because i don't see it appearing anywhere else. monkDB.get<{ code: string; userID: integer; createdOn: number }>("oauth2-auth-codes"), + "fer-settings": monkDB.get("fer-settings"), "orphan-chart-queue": monkDB.get("orphan-chart-queue"), "password-reset-codes": monkDB.get<{ diff --git a/server/src/external/mongo/indexes.ts b/server/src/external/mongo/indexes.ts index 7fe729520..ecd2df8e3 100644 --- a/server/src/external/mongo/indexes.ts +++ b/server/src/external/mongo/indexes.ts @@ -164,7 +164,7 @@ for (const game of TachiConfig.GAMES) { export async function SetIndexesWithDB(db: IMonkManager, reset: boolean) { const collections = (await db.listCollections()).map((e) => e.name); - for (const collection in indexes) { + for (const [collection, values] of Object.entries(indexes)) { if (!collections.includes(collection)) { // this creates a collection, i cant find the createCollection // call. @@ -179,8 +179,9 @@ export async function SetIndexesWithDB(db: IMonkManager, reset: boolean) { logger.debug(`Reset ${collection}.`); } - // @ts-expect-error dru(n)kts - for (const index of indexes[collection]) { + for (const index of values) { + // @ts-expect-error Type-mismatch here. our index.fileds are just boring records. I know + // that this sucks... const r = await db.get(collection).createIndex(index.fields, index.options); logger.debug(r); @@ -211,14 +212,17 @@ export function SetIndexesIfNoneSet() { logger.info( `No indexes on users, First-time Tachi-Server startup assumed. Running SetIndexes.` ); - SetIndexesWithDB(monkDB, true); + return SetIndexesWithDB(monkDB, true); } }) - .catch((err) => { + .catch((err: unknown) => { logger.info( `Error in finding users collection. First time startup likely. Running SetIndexes.`, - err + { err } ); - SetIndexesWithDB(monkDB, true); + return SetIndexesWithDB(monkDB, true); + }) + .catch((err: unknown) => { + logger.error(`Failed to set indexes on assumed first-time-setup?`, { err }); }); } diff --git a/server/src/external/redis/redis-IPC.test.ts b/server/src/external/redis/redis-IPC.test.ts index e29167d37..118256808 100644 --- a/server/src/external/redis/redis-IPC.test.ts +++ b/server/src/external/redis/redis-IPC.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import t from "tap"; import { RedisPub, RedisSub } from "./redis-IPC"; +import t from "tap"; /** * Note: The functions these things use have been stubbed out. However, the tests still run and diff --git a/server/src/external/redis/redis-IPC.ts b/server/src/external/redis/redis-IPC.ts index ac752cbe2..346d9a156 100644 --- a/server/src/external/redis/redis-IPC.ts +++ b/server/src/external/redis/redis-IPC.ts @@ -21,11 +21,11 @@ export interface RedisIPCData { type RedisSubCallback = (data: RedisIPCData[T]) => void; -type SubCallbacks = { +type SubCallbacksType = { [K in RedisIPCChannels]: Array>; }; -const SubCallbacks: Partial = {}; +const SubCallbacks: Partial = {}; // Redis doesn't allow one client to subscribe and publish // There's little to no overhead to having these three clients, @@ -55,16 +55,18 @@ export function RedisSub(channel: T, callback: Redis SubClient.on("message", (channel, strData) => { if (!channel.startsWith(`${PREFIX}-`)) { - return; // not our business + // not our business + return; } const ktChannel = channel.slice(`${PREFIX}-`.length) as RedisIPCChannels; if (!Object.prototype.hasOwnProperty.call(SubCallbacks, ktChannel)) { - return; // no callbacks to call + // no callbacks to call + return; } - const jsData = JSON.parse(strData); + const jsData = JSON.parse(strData) as unknown; for (const cb of SubCallbacks[ktChannel]!) { try { @@ -80,14 +82,14 @@ SubClient.on("message", (channel, strData) => { /* istanbul ignore next */ export function CloseRedisPubSub() { return new Promise((resolve, reject) => { - PubClient.quit((err) => { - if (err) { + PubClient.quit((err: unknown) => { + if (err !== null && err !== undefined) { logger.crit(`PubClient QUIT error: ${err}`, { err }); reject(err); } - SubClient.quit((err) => { - if (err) { + SubClient.quit((err: unknown) => { + if (err !== null && err !== undefined) { logger.crit(`SubClient QUIT error: ${err}`, { err }); reject(err); } diff --git a/server/src/external/redis/redis.ts b/server/src/external/redis/redis.ts index 05555690f..497e507b2 100644 --- a/server/src/external/redis/redis.ts +++ b/server/src/external/redis/redis.ts @@ -35,13 +35,13 @@ RedisClient.on("connect", () => { }); export function CloseRedisConnection() { - return new Promise((resolve, reject) => + return new Promise((resolve, reject) => { RedisClient.quit((err, reply) => { if (err) { reject(err); } else { resolve(reply); } - }) - ); + }); + }); } diff --git a/server/src/lib/cdn/cdn.test.ts b/server/src/lib/cdn/cdn.test.ts index d8a6c241f..4a16843ab 100644 --- a/server/src/lib/cdn/cdn.test.ts +++ b/server/src/lib/cdn/cdn.test.ts @@ -1,9 +1,9 @@ -import expressRequestMock from "express-request-mock"; -import fs from "fs"; -import { Environment, ServerConfig } from "lib/setup/config"; -import path from "path"; -import t from "tap"; import { CDNDelete, CDNRedirect, CDNRetrieve, CDNStoreOrOverwrite } from "./cdn"; +import expressRequestMock from "express-request-mock"; +import { Environment, ServerConfig } from "lib/setup/config"; +import t from "tap"; +import fs from "fs"; +import path from "path"; if (ServerConfig.CDN_CONFIG.SAVE_LOCATION.TYPE !== "LOCAL_FILESYSTEM") { throw new Error(`Tests must run against LOCAL_FILESYSTEM.`); @@ -122,7 +122,9 @@ t.test("#CDNRedirect", (t) => { t.beforeEach(ResetFileRoot); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const mockMW = (req: any, res: any) => CDNRedirect(res, "/test.txt"); + const mockMW = (req: any, res: any) => { + CDNRedirect(res, "/test.txt"); + }; t.test("Should redirect a user to the CDN Url", async (t) => { const { res } = await expressRequestMock(mockMW, {}); diff --git a/server/src/lib/constants/classes.ts b/server/src/lib/constants/classes.ts index 144e49f70..6a71a3018 100644 --- a/server/src/lib/constants/classes.ts +++ b/server/src/lib/constants/classes.ts @@ -1,243 +1,243 @@ export enum IIDXDans { - KYU_7, - KYU_6, - KYU_5, - KYU_4, - KYU_3, - KYU_2, - KYU_1, + KYU_7 = 0, + KYU_6 = 1, + KYU_5 = 2, + KYU_4 = 3, + KYU_3 = 4, + KYU_2 = 5, + KYU_1 = 6, - DAN_1, - DAN_2, - DAN_3, - DAN_4, - DAN_5, - DAN_6, - DAN_7, - DAN_8, - DAN_9, - DAN_10, + DAN_1 = 7, + DAN_2 = 8, + DAN_3 = 9, + DAN_4 = 10, + DAN_5 = 11, + DAN_6 = 12, + DAN_7 = 13, + DAN_8 = 14, + DAN_9 = 15, + DAN_10 = 16, - CHUUDEN, - KAIDEN, + CHUUDEN = 17, + KAIDEN = 18, } export enum GitadoraColours { - WHITE, + WHITE = 0, - ORANGE, - ORANGE_GRADIENT, - YELLOW, - YELLOW_GRADIENT, - GREEN, - GREEN_GRADIENT, - BLUE, - BLUE_GRADIENT, - PURPLE, - PURPLE_GRADIENT, - RED, - RED_GRADIENT, + ORANGE = 1, + ORANGE_GRADIENT = 2, + YELLOW = 3, + YELLOW_GRADIENT = 4, + GREEN = 5, + GREEN_GRADIENT = 6, + BLUE = 7, + BLUE_GRADIENT = 8, + PURPLE = 9, + PURPLE_GRADIENT = 10, + RED = 11, + RED_GRADIENT = 12, - BRONZE, - SILVER, - GOLD, - RAINBOW, + BRONZE = 13, + SILVER = 14, + GOLD = 15, + RAINBOW = 16, } export enum BMSGenocideDans { - NORMAL_1, - NORMAL_2, - NORMAL_3, - NORMAL_4, - NORMAL_5, - NORMAL_6, - NORMAL_7, - NORMAL_8, - NORMAL_9, - NORMAL_10, + NORMAL_1 = 0, + NORMAL_2 = 1, + NORMAL_3 = 2, + NORMAL_4 = 3, + NORMAL_5 = 4, + NORMAL_6 = 5, + NORMAL_7 = 6, + NORMAL_8 = 7, + NORMAL_9 = 8, + NORMAL_10 = 9, - INSANE_1, - INSANE_2, - INSANE_3, - INSANE_4, - INSANE_5, - INSANE_6, - INSANE_7, - INSANE_8, - INSANE_9, - INSANE_10, - INSANE_KAIDEN, + INSANE_1 = 10, + INSANE_2 = 11, + INSANE_3 = 12, + INSANE_4 = 13, + INSANE_5 = 14, + INSANE_6 = 15, + INSANE_7 = 16, + INSANE_8 = 17, + INSANE_9 = 18, + INSANE_10 = 19, + INSANE_KAIDEN = 20, - OVERJOY, + OVERJOY = 21, } export enum BMSStSlDans { - SL_0, - SL_1, - SL_2, - SL_3, - SL_4, - SL_5, - SL_6, - SL_7, - SL_8, - SL_9, - SL_10, - SL_11, - SL_12, + SL_0 = 0, + SL_1 = 1, + SL_2 = 2, + SL_3 = 3, + SL_4 = 4, + SL_5 = 5, + SL_6 = 6, + SL_7 = 7, + SL_8 = 8, + SL_9 = 9, + SL_10 = 10, + SL_11 = 11, + SL_12 = 12, - ST_0, - ST_1, - ST_2, - ST_3, - ST_4, - ST_5, - ST_6, - ST_7, - ST_8, - ST_9, - ST_10, - ST_11, - ST_12, + ST_0 = 13, + ST_1 = 14, + ST_2 = 15, + ST_3 = 16, + ST_4 = 17, + ST_5 = 18, + ST_6 = 19, + ST_7 = 20, + ST_8 = 21, + ST_9 = 22, + ST_10 = 23, + ST_11 = 24, + ST_12 = 25, } export enum PMSInsaneDans { - DAN_1, - DAN_2, - DAN_3, - DAN_4, - DAN_5, - DAN_6, - DAN_7, - DAN_8, - DAN_9, - DAN_10, - KAIDEN, - OVERJOY, - UNDEFINED, + DAN_1 = 0, + DAN_2 = 1, + DAN_3 = 2, + DAN_4 = 3, + DAN_5 = 4, + DAN_6 = 5, + DAN_7 = 6, + DAN_8 = 7, + DAN_9 = 8, + DAN_10 = 9, + KAIDEN = 10, + OVERJOY = 11, + UNDEFINED = 12, } export enum SDVXDans { - DAN_1, - DAN_2, - DAN_3, - DAN_4, - DAN_5, - DAN_6, - DAN_7, - DAN_8, - DAN_9, - DAN_10, - DAN_11, - INF, + DAN_1 = 0, + DAN_2 = 1, + DAN_3 = 2, + DAN_4 = 3, + DAN_5 = 4, + DAN_6 = 5, + DAN_7 = 6, + DAN_8 = 7, + DAN_9 = 8, + DAN_10 = 9, + DAN_11 = 10, + INF = 11, } // ewww export enum SDVXVFClasses { - SIENNA_I, - SIENNA_II, - SIENNA_III, - SIENNA_IV, - COBALT_I, - COBALT_II, - COBALT_III, - COBALT_IV, - DANDELION_I, - DANDELION_II, - DANDELION_III, - DANDELION_IV, - CYAN_I, - CYAN_II, - CYAN_III, - CYAN_IV, - SCARLET_I, - SCARLET_II, - SCARLET_III, - SCARLET_IV, - CORAL_I, - CORAL_II, - CORAL_III, - CORAL_IV, - ARGENTO_I, - ARGENTO_II, - ARGENTO_III, - ARGENTO_IV, - ELDORA_I, - ELDORA_II, - ELDORA_III, - ELDORA_IV, - CRIMSON_I, - CRIMSON_II, - CRIMSON_III, - CRIMSON_IV, - IMPERIAL_I, - IMPERIAL_II, - IMPERIAL_III, - IMPERIAL_IV, + SIENNA_I = 0, + SIENNA_II = 1, + SIENNA_III = 2, + SIENNA_IV = 3, + COBALT_I = 4, + COBALT_II = 5, + COBALT_III = 6, + COBALT_IV = 7, + DANDELION_I = 8, + DANDELION_II = 9, + DANDELION_III = 10, + DANDELION_IV = 11, + CYAN_I = 12, + CYAN_II = 13, + CYAN_III = 14, + CYAN_IV = 15, + SCARLET_I = 16, + SCARLET_II = 17, + SCARLET_III = 18, + SCARLET_IV = 19, + CORAL_I = 20, + CORAL_II = 21, + CORAL_III = 22, + CORAL_IV = 23, + ARGENTO_I = 24, + ARGENTO_II = 25, + ARGENTO_III = 26, + ARGENTO_IV = 27, + ELDORA_I = 28, + ELDORA_II = 29, + ELDORA_III = 30, + ELDORA_IV = 31, + CRIMSON_I = 32, + CRIMSON_II = 33, + CRIMSON_III = 34, + CRIMSON_IV = 35, + IMPERIAL_I = 36, + IMPERIAL_II = 37, + IMPERIAL_III = 38, + IMPERIAL_IV = 39, } export enum CHUNITHM_COLOURS { - BLUE, - GREEN, - ORANGE, - RED, - PURPLE, - BRONZE, - SILVER, - GOLD, - PLATINUM, - RAINBOW, + BLUE = 0, + GREEN = 1, + ORANGE = 2, + RED = 3, + PURPLE = 4, + BRONZE = 5, + SILVER = 6, + GOLD = 7, + PLATINUM = 8, + RAINBOW = 9, } export enum WACCA_STAGEUPS { - I, - II, - III, - IV, - V, - VI, - VII, - VIII, - IX, - X, - XI, - XII, - XIII, - XIV, + I = 0, + II = 1, + III = 2, + IV = 3, + V = 4, + VI = 5, + VII = 6, + VIII = 7, + IX = 8, + X = 9, + XI = 10, + XII = 11, + XIII = 12, + XIV = 13, } export enum WACCA_COLOURS { - ASH, - NAVY, - YELLOW, - RED, - PURPLE, - BLUE, - SILVER, - GOLD, - RAINBOW, + ASH = 0, + NAVY = 1, + YELLOW = 2, + RED = 3, + PURPLE = 4, + BLUE = 5, + SILVER = 6, + GOLD = 7, + RAINBOW = 8, } export enum POPN_CLASSES { - KITTY, - GRADE_SCHOOL, - DELINQUENT, - DETECTIVE, - IDOL, - GENERAL, - HERMIT, - GOD, + KITTY = 0, + GRADE_SCHOOL = 1, + DELINQUENT = 2, + DETECTIVE = 3, + IDOL = 4, + GENERAL = 5, + HERMIT = 6, + GOD = 7, } export enum JUBEAT_COLOURS { - BLACK, - YELLOW_GREEN, - GREEN, - LIGHT_BLUE, - BLUE, - VIOLET, - PURPLE, - PINK, - ORANGE, - GOLD, + BLACK = 0, + YELLOW_GREEN = 1, + GREEN = 2, + LIGHT_BLUE = 3, + BLUE = 4, + VIOLET = 5, + PURPLE = 6, + PINK = 7, + ORANGE = 8, + GOLD = 9, } diff --git a/server/src/lib/constants/err-codes.ts b/server/src/lib/constants/err-codes.ts index 6cc16fa3f..ef86181cd 100644 --- a/server/src/lib/constants/err-codes.ts +++ b/server/src/lib/constants/err-codes.ts @@ -1,10 +1,10 @@ export enum SubscribeFailReasons { - ALREADY_SUBSCRIBED, - ALREADY_ACHIEVED, + ALREADY_SUBSCRIBED = 0, + ALREADY_ACHIEVED = 1, } export enum SetRivalsFailReasons { - TOO_MANY, - RIVALED_SELF, - RIVALS_HAVENT_PLAYED_GPT, + TOO_MANY = 0, + RIVALED_SELF = 1, + RIVALS_HAVENT_PLAYED_GPT = 2, } diff --git a/server/src/lib/constants/version.ts b/server/src/lib/constants/version.ts index 20a267eae..13783616f 100644 --- a/server/src/lib/constants/version.ts +++ b/server/src/lib/constants/version.ts @@ -11,17 +11,39 @@ const PATCH = 0; // The fact that this album is very similar to Mezzanine // - the album used for Kamaitachi v1 - is deliberate. :) - zkldi const PORTISHEAD_DUMMY = [ - "Mysterons", // v2.0.0 - "Sour Times", // v2.1.0 - "Strangers", // v2.2.0 - "It Could Be Sweet", // v2.3.0 - "Wandering Star", // v2.4.0 - "It's a Fire", // v2.5.0 - "Numb", // v2.6.0 - "Roads", // v2.7.0 - "Pedestal", // v2.8.0 - "Biscuit", // v2.9.0 - "Glory Box", // v2.10.0 + // v2.0.0 + "Mysterons", + + // v2.1.0 + "Sour Times", + + // v2.2.0 + "Strangers", + + // v2.3.0 + "It Could Be Sweet", + + // v2.4.0 + "Wandering Star", + + // v2.5.0 + "It's a Fire", + + // v2.6.0 + "Numb", + + // v2.7.0 + "Roads", + + // v2.8.0 + "Pedestal", + + // v2.9.0 + "Biscuit", + + // v2.10.0 + "Glory Box", + // v2.11+ onwards are not expected to occur, but if they do, we'll figure something out. ]; diff --git a/server/src/lib/database-seeds/repo.ts b/server/src/lib/database-seeds/repo.ts index fc31d0fda..5989a8fc9 100644 --- a/server/src/lib/database-seeds/repo.ts +++ b/server/src/lib/database-seeds/repo.ts @@ -172,7 +172,7 @@ export async function PullDatabaseSeeds() { } return new DatabaseSeedsRepo(seedsDir); - } catch ({ err, stdout, stderr }) { + } catch ({ err, stderr }) { logger.error(`Error cloning database-seeds. ${stderr}.`); throw err; } diff --git a/server/src/lib/email/client.ts b/server/src/lib/email/client.ts index fbfd4ecba..c9627bb4b 100644 --- a/server/src/lib/email/client.ts +++ b/server/src/lib/email/client.ts @@ -13,8 +13,10 @@ if (ServerConfig.EMAIL_CONFIG) { const conf = ServerConfig.EMAIL_CONFIG; try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument transporter = nodemailer.createTransport({ newline: "unix", + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unsafe-member-access logger: conf.TRANSPORT_OPS?.debug ? bunyan.createLogger({ name: "Email Logger" }) : undefined, diff --git a/server/src/lib/game-settings/create-game-settings.test.ts b/server/src/lib/game-settings/create-game-settings.test.ts index f7d35edfe..3665b770e 100644 --- a/server/src/lib/game-settings/create-game-settings.test.ts +++ b/server/src/lib/game-settings/create-game-settings.test.ts @@ -1,7 +1,7 @@ +import { CreateGameSettings } from "./create-game-settings"; import db from "external/mongo/db"; import t from "tap"; import ResetDBState from "test-utils/resets"; -import { CreateGameSettings } from "./create-game-settings"; t.test("#CreateGameSettings", (t) => { t.beforeEach(ResetDBState); diff --git a/server/src/lib/handlers/sigterm.ts b/server/src/lib/handlers/sigterm.ts index 2b5947fbb..cec96fcde 100644 --- a/server/src/lib/handlers/sigterm.ts +++ b/server/src/lib/handlers/sigterm.ts @@ -11,11 +11,9 @@ export function HandleSIGTERMGracefully(instance?: http.Server | https.Server) { logger.info("SIGTERM Received, closing program.", { shutdownInfo: true }); if (instance) { - instance.close(() => { - CloseEverythingElse(); - }); + instance.close(() => CloseEverythingElse()); } else { - CloseEverythingElse(); + return CloseEverythingElse(); } } @@ -24,10 +22,10 @@ async function CloseEverythingElse() { await monkDB.close(); logger.info("Closing Redis Connection.", { shutdownInfo: true }); - CloseRedisConnection(); + await CloseRedisConnection(); logger.info("Closing Score Import Queue.", { shutdownInfo: true }); - CloseScoreImportQueue(); + await CloseScoreImportQueue(); logger.info("Everything closed. Waiting for process to exit naturally.", { shutdownInfo: true, diff --git a/server/src/lib/imports/imports.test.ts b/server/src/lib/imports/imports.test.ts index c1ecfb8db..73c5f0bf6 100644 --- a/server/src/lib/imports/imports.test.ts +++ b/server/src/lib/imports/imports.test.ts @@ -1,9 +1,9 @@ +import { RevertImport } from "./imports"; import db from "external/mongo/db"; import t from "tap"; import { dmf, mkFakeImport } from "test-utils/misc"; import ResetDBState from "test-utils/resets"; import { TestingIIDXSPScore } from "test-utils/test-data"; -import { RevertImport } from "./imports"; t.test("#RevertImport", (t) => { t.beforeEach(ResetDBState); @@ -30,6 +30,7 @@ t.test("#RevertImport", (t) => { t.resolveMatch( db.scores.findOne({ userID: 1, scoreID: "score_1" }), + // @ts-expect-error https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60020 null, "Score_1 should be removed from the database." @@ -37,6 +38,7 @@ t.test("#RevertImport", (t) => { t.resolveMatch( db.scores.findOne({ userID: 1, scoreID: "score_2" }), + // @ts-expect-error see above null, "Score_2 should be removed from the database." @@ -44,6 +46,7 @@ t.test("#RevertImport", (t) => { t.resolveMatch( db.scores.findOne({ userID: 1, scoreID: "score_3" }), + // @ts-expect-error see above { scoreID: "score_3" }, "Score_2 should NOT be removed from the database." diff --git a/server/src/lib/imports/imports.ts b/server/src/lib/imports/imports.ts index 30b66bf58..e7f49d263 100644 --- a/server/src/lib/imports/imports.ts +++ b/server/src/lib/imports/imports.ts @@ -31,7 +31,7 @@ export async function RevertImport(importDoc: ImportDocument) { } catch (err) { logger.severe( `Deleted scores that were part of import, but failed to remove the actual import? There is a stale import with ID '${importDoc.importID}', which must be removed manually.`, - { importDoc } + { importDoc, err } ); } } diff --git a/server/src/lib/jobs/backsync-bms-pms-data.ts b/server/src/lib/jobs/backsync-bms-pms-data.ts index c85db8f3e..13474c5f8 100644 --- a/server/src/lib/jobs/backsync-bms-pms-data.ts +++ b/server/src/lib/jobs/backsync-bms-pms-data.ts @@ -42,5 +42,11 @@ export async function BacksyncBMSPMSSongsAndCharts() { } if (require.main === module) { - BacksyncBMSPMSSongsAndCharts().then(() => process.exit(0)); + BacksyncBMSPMSSongsAndCharts() + .then(() => process.exit(0)) + .catch((err: unknown) => { + logger.error(`Failed to backsync bms/pms songs and charts.`, { err }); + + process.exit(1); + }); } diff --git a/server/src/lib/jobs/bms-table-sync.ts b/server/src/lib/jobs/bms-table-sync.ts index 6ed940e8f..a8dc8e198 100644 --- a/server/src/lib/jobs/bms-table-sync.ts +++ b/server/src/lib/jobs/bms-table-sync.ts @@ -1,7 +1,7 @@ +/* eslint-disable no-await-in-loop */ + import db from "external/mongo/db"; import { BMS_TABLES } from "lib/constants/bms-tables"; - -/* eslint-disable no-await-in-loop */ import CreateLogCtx from "lib/logger/logger"; import fetch from "node-fetch"; import { CreateFolderID } from "utils/folder"; @@ -127,7 +127,7 @@ async function ImportTableLevels(tableJSON: Array, prefix: string) for (const td of tableJSON) { const chart = (await db.charts.bms.findOne({ "data.hashMD5": td.md5 })) as ChartDocument< "bms:7K" | "bms:14K" - >; + > | null; if (!chart) { logger.warn( @@ -212,7 +212,9 @@ export async function UpdateTable(table: BMSTablesDataset) { try { text = await res.text(); - tableJSON = JSON.parse(text); + + // @hack -- this is a force cast. + tableJSON = JSON.parse(text) as unknown as Array; } catch (err) { logger.error(`Failed to fetch ${table.url}`, { err, res, statusCode, text }); retries++; @@ -302,5 +304,9 @@ if (require.main === module) { } process.exit(0); - })(); + })().catch((err: unknown) => { + logger.error(`Failed to sync BMS Tables.`, { err }); + + process.exit(1); + }); } diff --git a/server/src/lib/jobs/deorphan-scores.ts b/server/src/lib/jobs/deorphan-scores.ts index 199738767..fdbb5ae0a 100644 --- a/server/src/lib/jobs/deorphan-scores.ts +++ b/server/src/lib/jobs/deorphan-scores.ts @@ -42,5 +42,11 @@ export async function DeoprhanScores() { } if (require.main === module) { - DeoprhanScores().then(() => process.exit(0)); + DeoprhanScores() + .then(() => process.exit(0)) + .catch((err: unknown) => { + logger.error(`Failed to de-orphan scores.`, { err }); + + process.exit(1); + }); } diff --git a/server/src/lib/jobs/inline-job-runner/job-runner.ts b/server/src/lib/jobs/inline-job-runner/job-runner.ts index 93d3e8e53..60e2cf0e6 100644 --- a/server/src/lib/jobs/inline-job-runner/job-runner.ts +++ b/server/src/lib/jobs/inline-job-runner/job-runner.ts @@ -74,19 +74,19 @@ export function InitialiseJobRunner() { const jobNameMap = new Map(); for (const job of jobs) { - JobQueue.add(job.name, { jobName: job.name }, { repeat: { cron: job.cronFormat } }); + void JobQueue.add(job.name, { jobName: job.name }, { repeat: { cron: job.cronFormat } }); jobNameMap.set(job.name, job); } - new Worker("Job Runner", async (j) => { - const name = j.data.jobName; + const worker = new Worker("Job Runner", async (j) => { + const { jobName } = j.data as { jobName: string }; - logger.info(`Running job ${name}.`); + logger.info(`Running job ${jobName}.`); - const jobInfo = jobNameMap.get(name); + const jobInfo = jobNameMap.get(jobName); if (!jobInfo) { - logger.severe(`Unknown job name ${name}, couldn't find a run function?`); + logger.severe(`Unknown job name ${jobName}, couldn't find a run function?`); return false; } @@ -96,6 +96,8 @@ export function InitialiseJobRunner() { }); logger.info(`Initialised ${jobs.length} jobs (${jobs.map((e) => e.name).join(", ")}).`); + + return worker; } if (require.main === module) { diff --git a/server/src/lib/jobs/ugs-snapshot.ts b/server/src/lib/jobs/ugs-snapshot.ts index 2fcf4817b..e0fcfdc38 100644 --- a/server/src/lib/jobs/ugs-snapshot.ts +++ b/server/src/lib/jobs/ugs-snapshot.ts @@ -32,11 +32,13 @@ export async function UGSSnapshot() { logger.info(`Snapshotting UserGameStats.`); try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call await db["game-stats"] .find({}) // @ts-expect-error faulty TS types .each(async (ugs: UserGameStats, { pause, resume }) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call pause(); logger.debug(`Snapshotting ${ugs.userID} ${ugs.playtype} ${ugs.game}.`); @@ -58,9 +60,11 @@ export async function UGSSnapshot() { if (batchWrite.length >= 500) { logger.verbose(`Flushed batch.`); await db["game-stats-snapshots"].insert(batchWrite); + // eslint-disable-next-line require-atomic-updates batchWrite = []; } + // eslint-disable-next-line @typescript-eslint/no-unsafe-call resume(); }); @@ -90,7 +94,14 @@ export async function UGSSnapshot() { } if (require.main === module) { - UGSSnapshot().then(() => { - process.exit(0); - }); + UGSSnapshot() + .then(() => { + process.exit(0); + }) + .catch((err: unknown) => { + // This is a severe error, not an error. Running the UGS snapshot every day is necessary. + logger.severe(`Failed to snapshot user game stats.`, { err }); + + process.exit(1); + }); } diff --git a/server/src/lib/jobs/update-dp-tiers.ts b/server/src/lib/jobs/update-dp-tiers.ts index a7ed51605..1aaa69ad1 100644 --- a/server/src/lib/jobs/update-dp-tiers.ts +++ b/server/src/lib/jobs/update-dp-tiers.ts @@ -120,7 +120,7 @@ function ParseTierStr(tierStr: string) { const result = /\((.*)\)$/u.exec(tierStr); - if (result && result[1]) { + if (result?.[1]) { return Number(result[1]); } @@ -128,7 +128,13 @@ function ParseTierStr(tierStr: string) { } if (require.main === module) { - UpdateDPTiers().then(() => { - process.exit(0); - }); + UpdateDPTiers() + .then(() => { + process.exit(0); + }) + .catch((err: unknown) => { + logger.error(`Failed to update DP Tiers.`, { err }); + + process.exit(1); + }); } diff --git a/server/src/lib/logger/colours.ts b/server/src/lib/logger/colours.ts index ac4c84ef4..26da457ba 100644 --- a/server/src/lib/logger/colours.ts +++ b/server/src/lib/logger/colours.ts @@ -11,10 +11,19 @@ const Colours = { // Discord ONLY accepts decimal colours, so. // lets run with that. export const DiscordColours = { - crit: 16711680, // #ff0000 - severe: 16719904, // #ff3333 - error: 11730944, // #cc0000 - warn: 16703232, // #ffcc00 - // other ones dont matter + // #ff0000 + crit: 16711680, + + // #ff3333 + severe: 16719904, + + // #cc0000 + error: 11730944, + + // #ffcc00 + warn: 16703232, + + // other log colours don't matter -- they can all default to gray }; + export default Colours; diff --git a/server/src/lib/logger/discord-transport.ts b/server/src/lib/logger/discord-transport.ts index d322e7082..bdd724167 100644 --- a/server/src/lib/logger/discord-transport.ts +++ b/server/src/lib/logger/discord-transport.ts @@ -4,11 +4,11 @@ import { ONE_MINUTE } from "lib/constants/time"; import { Environment, ServerConfig, TachiConfig } from "lib/setup/config"; import SafeJSONStringify from "safe-json-stringify"; import fetch from "utils/fetch"; +import { IsRecord } from "utils/misc"; import Transport from "winston-transport"; import type { TransportStreamOptions } from "winston-transport"; interface DiscordTransportOptions extends TransportStreamOptions { - /** Webhook obtained from Discord */ webhook: string; } @@ -17,6 +17,13 @@ interface LogLevelCountState { error: Array; } +interface LoggerInfo { + level: "crit" | "debug" | "info" | "severe" | "verbose" | "warn"; + message: string; + noDiscord?: boolean; + meta?: Record; +} + /** * Creates a discord winston transport. This is a slightly adapted version * of sidhantpanda's winston-discord-transport, modified for our use case. @@ -49,9 +56,15 @@ export default class DiscordTransport extends Transport { throw new Error(`Couldn't connect to discord transport. ${r.status}.`); } - return r.json(); + return r.json() as unknown; }) .then((content) => { + if (!IsRecord(content)) { + throw new Error( + `Discord returned non-object data as JSON? Can't get id or token ${content}.` + ); + } + this.webhookUrl = `https://discordapp.com/api/v6/webhooks/${content.id}/${content.token}`; }); } @@ -63,12 +76,12 @@ export default class DiscordTransport extends Transport { }; } - log(info: any, cb: () => void) { + log(info: LoggerInfo, cb: () => void) { if (info.noDiscord !== false) { try { setImmediate(() => { - this.initalised.then(() => { - this.handleSendToDiscord(info); + void this.initalised.then(() => { + void this.handleSendToDiscord(info); }); }); } catch (err) { @@ -81,7 +94,7 @@ export default class DiscordTransport extends Transport { cb(); } - private handleSendToDiscord(info: any) { + private handleSendToDiscord(info: LoggerInfo) { if (info.level === "crit" || info.level === "severe") { return this.sendLogDirectlyToDiscord(info); } @@ -103,8 +116,8 @@ export default class DiscordTransport extends Transport { } private getWhoToTag() { - return ServerConfig.LOGGER_CONFIG.DISCORD!.WHO_TO_TAG - ? ServerConfig.LOGGER_CONFIG.DISCORD!.WHO_TO_TAG.map((e) => `<@${e}>`).join(" ") + return ServerConfig.LOGGER_CONFIG.DISCORD?.WHO_TO_TAG + ? ServerConfig.LOGGER_CONFIG.DISCORD.WHO_TO_TAG.map((e) => `<@${e}>`).join(" ") : "Nobody configured to tag, but this is bad, get someone!"; } @@ -147,13 +160,13 @@ export default class DiscordTransport extends Transport { ], }; - this.POSTData(postBody); + void this.POSTData(postBody); this.resetBucketData(); this.isBucketing = false; this.bucketStart = null; } - private async sendLogDirectlyToDiscord(info: any) { + private async sendLogDirectlyToDiscord(info: LoggerInfo) { const postBody = { content: "", embeds: [ @@ -195,7 +208,7 @@ export default class DiscordTransport extends Transport { if (res.status === 429) { // being rate limited. - const content = await res.json(); + const content = (await res.json()) as unknown; // Try and retry when they say so. The issue is that our logging // is very async, and this is a billion race conditions. @@ -207,9 +220,9 @@ export default class DiscordTransport extends Transport { // It's possible this might blow up in our face. // We'll have to see. - zkldi 2021/09/17 - if (content.retry_after) { + if (IsRecord(content) && typeof content.retry_after === "number") { setTimeout(() => { - this.POSTData(postBody, scaleRetryDebounce ** 2); + void this.POSTData(postBody, scaleRetryDebounce ** 2); }, content.retry_after * scaleRetryDebounce); } } else if (!res.ok) { diff --git a/server/src/lib/logger/logger.test.ts b/server/src/lib/logger/logger.test.ts index 441539009..9569c533a 100644 --- a/server/src/lib/logger/logger.test.ts +++ b/server/src/lib/logger/logger.test.ts @@ -1,18 +1,18 @@ +import CreateLogCtx, { ChangeRootLogLevel, GetLogLevel, rootLogger, Transports } from "./logger"; import { ServerConfig } from "lib/setup/config"; import t from "tap"; -import CreateLogCtx, { ChangeRootLogLevel, GetLogLevel, rootLogger, Transports } from "./logger"; - const LOG_LEVEL = ServerConfig.LOGGER_CONFIG.LOG_LEVEL; t.test("Logger Tests", (t) => { const logger = CreateLogCtx(__filename); if (!Transports[0]) { - throw new Error(`No transports were defined? Can't perform logger tests.`) + throw new Error(`No transports were defined? Can't perform logger tests.`); } - Transports[0].level = "debug"; // lol + // lol + Transports[0].level = "debug"; logger.debug("Debug Message Test"); logger.verbose("Verbose Message Test"); diff --git a/server/src/lib/logger/logger.ts b/server/src/lib/logger/logger.ts index d5d25f587..aca856315 100644 --- a/server/src/lib/logger/logger.ts +++ b/server/src/lib/logger/logger.ts @@ -1,12 +1,12 @@ +import DiscordWinstonTransport from "./discord-transport"; import { Transport as SeqTransport } from "@valuabletouch/winston-seq"; import { Environment, ServerConfig, TachiConfig } from "lib/setup/config"; import SafeJSONStringify from "safe-json-stringify"; -import { EscapeStringRegexp, IsNonEmptyString } from "utils/misc"; +import { EscapeStringRegexp } from "utils/misc"; import winston, { format, transports } from "winston"; import type { SeqLogLevel } from "seq-logging"; import type { LeveledLogMethod, Logger } from "winston"; import "winston-daily-rotate-file"; -import DiscordWinstonTransport from "./discord-transport"; export type KtLogger = Logger & { severe: LeveledLogMethod }; @@ -15,9 +15,7 @@ const level = process.env.LOG_LEVEL ?? ServerConfig.LOGGER_CONFIG.LOG_LEVEL; const formatExcessProperties = (meta: Record, limit = false) => { let i = 0; - for (const key in meta) { - const val = meta[key]; - + for (const [key, val] of Object.entries(meta)) { if (val instanceof Error) { meta[key] = { message: val.message, stack: val.stack }; } @@ -49,13 +47,11 @@ const formatExcessPropertiesNoStack = ( ) => { const realMeta: Record = {}; - for (const key in meta) { + for (const [key, val] of Object.entries(meta)) { if (omitKeys.includes(key)) { continue; } - const val = meta[key]; - if (val instanceof Error) { realMeta[key] = { message: val.message }; } else if (!key.startsWith("__") && !key.startsWith("!")) { @@ -85,7 +81,11 @@ const tachiConsolePrintf = format.printf( ({ level, message, context = "tachi-root", timestamp, hideFromConsole, ...meta }) => `${timestamp}${replicaInfo} [${ Array.isArray(context) ? context.join(" | ") : context - }] ${level}: ${message}${formatExcessPropertiesNoStack(meta, hideFromConsole, true)}` + }] ${level}: ${message}${formatExcessPropertiesNoStack( + meta, + hideFromConsole as Array, + true + )}` ); winston.addColors({ @@ -241,11 +241,15 @@ function CreateLogCtx(filename: string, lg = rootLogger): KtLogger { context: [replacedFilename], }) as KtLogger; + // @hack, defaultMeta isn't reactive -- won't be updated unless we do this. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment logger.defaultMeta = { ...(logger.defaultMeta ?? {}), context: [replacedFilename] }; + return logger; } export function AppendLogCtx(context: string, lg: KtLogger): KtLogger { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access const newContext = [...lg.defaultMeta.context, context]; return lg.child({ context: newContext }) as KtLogger; diff --git a/server/src/lib/migration/migrations.test.ts b/server/src/lib/migration/migrations.test.ts index 9a929dcdd..408c6f503 100644 --- a/server/src/lib/migration/migrations.test.ts +++ b/server/src/lib/migration/migrations.test.ts @@ -1,7 +1,7 @@ +import { ApplyMigration, FAKE_MIGRATION, FindUnappliedMigrations } from "./migrations"; import db from "external/mongo/db"; import t from "tap"; import ResetDBState from "test-utils/resets"; -import { ApplyMigration, FAKE_MIGRATION, FindUnappliedMigrations } from "./migrations"; t.test("#FindUnappliedMigrations", (t) => { t.beforeEach(ResetDBState); @@ -59,6 +59,7 @@ t.test("#ApplyMigration", (t) => { }); t.equal( + // eslint-disable-next-line lines-around-comment // @ts-expect-error Our migration is a fake one that adds __wasMigrated // to all user documents. There's nothing special about this property name. dbRes?.__wasMigrated, @@ -81,7 +82,8 @@ t.test("#ApplyMigration", (t) => { }); } catch (e) { const err = e as Error; - t.equal(err?.message, "Was going to exit with statusCode 1, but we're in testing."); + + t.equal(err.message, "Was going to exit with statusCode 1, but we're in testing."); } const dbRes = await db.users.findOne({ @@ -89,6 +91,7 @@ t.test("#ApplyMigration", (t) => { }); t.equal( + // eslint-disable-next-line lines-around-comment // @ts-expect-error Our un-migration adds this property to check if it worked. dbRes?.__wasUnmigrated, true, @@ -113,7 +116,8 @@ t.test("#ApplyMigration", (t) => { }); } catch (e) { const err = e as Error; - t.equal(err?.message, "Was going to exit with statusCode 1, but we're in testing."); + + t.equal(err.message, "Was going to exit with statusCode 1, but we're in testing."); } t.end(); diff --git a/server/src/lib/migration/migrations.ts b/server/src/lib/migration/migrations.ts index 3e8f6cc8f..515011982 100644 --- a/server/src/lib/migration/migrations.ts +++ b/server/src/lib/migration/migrations.ts @@ -18,9 +18,9 @@ export const FAKE_MIGRATION: Migration = { // Migrations are stored in an array because they have some concept of order // That is, migrations should ideally be applied in a fixed order just to avoid // any potential unsavoury interactions. +// If we're testing, we should pull fake migrations instead to ensure the tests +// stay consistent const REGISTERED_MIGRATIONS: Array = - // If we're testing, we should pull fake migrations instead to ensure the tests - // stay consistent Environment.nodeEnv === "test" ? [FAKE_MIGRATION] : [UGPTRivalsMigration]; function CreateMigrationLookupMap(migrations: Array) { diff --git a/server/src/lib/notifications/notifications.test.ts b/server/src/lib/notifications/notifications.test.ts index 37d57beca..eb67f2db9 100644 --- a/server/src/lib/notifications/notifications.test.ts +++ b/server/src/lib/notifications/notifications.test.ts @@ -1,7 +1,7 @@ +import { BulkSendNotification, SendNotification } from "./notifications"; import db from "external/mongo/db"; import t from "tap"; import ResetDBState from "test-utils/resets"; -import { BulkSendNotification, SendNotification } from "./notifications"; t.test("#SendNotification", (t) => { t.beforeEach(ResetDBState); diff --git a/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts b/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts index dfe825478..746b8dc0f 100644 --- a/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts +++ b/server/src/lib/score-import/framework/calculated-data/calculated-data.test.ts @@ -1,6 +1,6 @@ +import { CalculateDataForGamePT, CreateCalculatedData } from "./calculated-data"; 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 { @@ -10,7 +10,7 @@ import { TestingIIDXSPDryScore, TestingSDVXSingleDryScore, } from "test-utils/test-data"; -import { CalculateDataForGamePT, CreateCalculatedData } from "./calculated-data"; +import type { ChartDocument, ScoreDocument } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -101,7 +101,8 @@ t.test("#CalculateDataForGamePT", (t) => { "iidx", "DP", Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an SP score. but we're testing + + TestingIIDXSPDryScore, 30, logger @@ -147,7 +148,7 @@ t.test("#CalculateDataForGamePT", (t) => { "ddr", "SP", Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + TestingIIDXSPDryScore, null, logger ); @@ -170,7 +171,7 @@ t.test("#CalculateDataForGamePT", (t) => { "ddr", "DP", Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + TestingIIDXSPDryScore, null, logger ); @@ -193,7 +194,7 @@ t.test("#CalculateDataForGamePT", (t) => { "chunithm", "Single", Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + TestingIIDXSPDryScore, null, logger ); @@ -215,7 +216,7 @@ t.test("#CalculateDataForGamePT", (t) => { "maimai", "Single", Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + TestingIIDXSPDryScore, null, logger ); @@ -237,7 +238,7 @@ t.test("#CalculateDataForGamePT", (t) => { "museca", "Single", Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + TestingIIDXSPDryScore, null, logger ); @@ -259,7 +260,7 @@ t.test("#CalculateDataForGamePT", (t) => { "bms", "7K", Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + TestingIIDXSPDryScore, null, logger ); @@ -281,7 +282,7 @@ t.test("#CalculateDataForGamePT", (t) => { "bms", "14K", Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + TestingIIDXSPDryScore, null, logger ); @@ -303,7 +304,7 @@ t.test("#CalculateDataForGamePT", (t) => { "gitadora", "Gita", Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + TestingIIDXSPDryScore, null, logger ); @@ -325,7 +326,7 @@ t.test("#CalculateDataForGamePT", (t) => { "gitadora", "Dora", Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + TestingIIDXSPDryScore, null, logger ); @@ -347,7 +348,7 @@ t.test("#CalculateDataForGamePT", (t) => { "usc", "Controller", Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + TestingIIDXSPDryScore, null, logger ); @@ -369,7 +370,7 @@ t.test("#CalculateDataForGamePT", (t) => { "usc", "Keyboard", Testing511SPA, - TestingIIDXSPDryScore, // fake! this is an iidx score. but we're testing + TestingIIDXSPDryScore, null, logger ); diff --git a/server/src/lib/score-import/framework/calculated-data/calculated-data.ts b/server/src/lib/score-import/framework/calculated-data/calculated-data.ts index 68d802125..e80323a74 100644 --- a/server/src/lib/score-import/framework/calculated-data/calculated-data.ts +++ b/server/src/lib/score-import/framework/calculated-data/calculated-data.ts @@ -40,7 +40,7 @@ export async function CreateCalculatedData( return calculatedData; } -type CalculatedDataFunctions = { +type CalculatedDataFunctionsType = { [I in IDStrings]: ( dryScore: DryScore, chart: ChartDocument, @@ -48,7 +48,7 @@ type CalculatedDataFunctions = { ) => Promise["calculatedData"]> | ScoreDocument["calculatedData"]; }; -const CalculatedDataFunctions: CalculatedDataFunctions = { +const CalculatedDataFunctions: CalculatedDataFunctionsType = { "iidx:SP": CalculateDataIIDX, "iidx:DP": CalculateDataIIDX, "sdvx:Single": CalculateDataSDVXorUSC, @@ -92,11 +92,11 @@ type CalculatedData = Required["calculated function CalculateDataIIDX( dryScore: DryScore, chart: ChartDocument<"iidx:DP" | "iidx:SP">, - logger: KtLogger + _logger: KtLogger ): CalculatedData<"iidx:SP"> { let bpi; - if (chart.data.kaidenAverage && chart.data.worldRecord) { + if (chart.data.kaidenAverage !== null && chart.data.worldRecord !== null) { bpi = PoyashiBPI.calculate( dryScore.scoreData.score, chart.data.kaidenAverage, @@ -124,7 +124,7 @@ function CalculateDataIIDX( function CalculateDataSDVXorUSC( dryScore: DryScore, chart: ChartDocument, - logger: KtLogger + _logger: KtLogger ): CalculatedData<"sdvx:Single" | "usc:Controller" | "usc:Keyboard"> { // for usc, unofficial charts currently have no VF6 value. if ( @@ -156,7 +156,7 @@ function CalculateDataMuseca( function CalculateDataJubeat( dryScore: DryScore, chart: ChartDocument, - logger: KtLogger + _logger: KtLogger ): CalculatedData<"jubeat:Single"> { return { jubility: Jubility.calculate( @@ -199,7 +199,7 @@ function CalculateDataDDR( export function CalculateDataPMSorBMS( dryScore: DryScore, chart: ChartDocument, - logger: KtLogger + _logger: KtLogger ): CalculatedData<"pms:Controller" | "pms:Keyboard"> { const gptConfig = GetGamePTConfig(dryScore.game, chart.playtype); @@ -213,7 +213,7 @@ export function CalculateDataPMSorBMS( function CalculateDataWACCA( dryScore: DryScore, chart: ChartDocument, - logger: KtLogger + _logger: KtLogger ): CalculatedData<"wacca:Single"> { const rate = WACCARate.calculate(dryScore.scoreData.score, chart.levelNum); @@ -223,7 +223,7 @@ function CalculateDataWACCA( function CalculateDataPopn( dryScore: DryScore<"popn:9B">, chart: ChartDocument<"popn:9B">, - logger: KtLogger + _logger: KtLogger ): CalculatedData<"popn:9B"> { return { classPoints: PopnClassPoints.calculate( diff --git a/server/src/lib/score-import/framework/calculated-data/stats.test.ts b/server/src/lib/score-import/framework/calculated-data/stats.test.ts index 0073da3cc..920b24d0e 100644 --- a/server/src/lib/score-import/framework/calculated-data/stats.test.ts +++ b/server/src/lib/score-import/framework/calculated-data/stats.test.ts @@ -1,9 +1,9 @@ +import { CalculateKTLampRatingIIDXDP, CalculateKTLampRatingIIDXSP, CalculateMFCP } from "./stats"; import CreateLogCtx from "lib/logger/logger"; -import { ChartDocument, Difficulties, Lamps, ScoreDocument } from "tachi-common"; import t from "tap"; import { Testing511SPA, TestingIIDXSPDryScore } from "test-utils/test-data"; -import { DryScore } from "../common/types"; -import { CalculateKTLampRatingIIDXDP, CalculateKTLampRatingIIDXSP, CalculateMFCP } from "./stats"; +import type { DryScore } from "../common/types"; +import type { ChartDocument, Difficulties, Lamps, ScoreDocument } from "tachi-common"; t.test("#CalculateKTLampRatingIIDXSP", (t) => { function c( @@ -11,7 +11,8 @@ t.test("#CalculateKTLampRatingIIDXSP", (t) => { hc: number | null, exhc: number | null ): ChartDocument<"iidx:SP"> { - return Object.assign({}, Testing511SPA, { + return { + ...Testing511SPA, tierlistInfo: { "kt-EXHC": { value: exhc, @@ -23,15 +24,16 @@ t.test("#CalculateKTLampRatingIIDXSP", (t) => { value: nc, }, }, - }); + }; } - function s(lamp: Lamps["iidx:SP" | "iidx:DP"]): DryScore<"iidx:SP" | "iidx:DP"> { - return Object.assign({}, TestingIIDXSPDryScore, { + function s(lamp: Lamps["iidx:DP" | "iidx:SP"]): DryScore<"iidx:DP" | "iidx:SP"> { + return { + ...TestingIIDXSPDryScore, scoreData: { lamp, }, - }); + }; } t.equal( @@ -99,24 +101,27 @@ t.test("#CalculateKTLampRatingIIDXSP", (t) => { t.test("#CalculateKTLampRatingIIDXDP", (t) => { function c(dpTier: number | null): ChartDocument<"iidx:DP"> { - return Object.assign({}, Testing511SPA, { + return { + ...Testing511SPA, playtype: "DP", tierlistInfo: { "dp-tier": { value: dpTier, }, }, + // hack over-assert this type, since we're merging the SPA into being // a DPA. - }) as unknown as ChartDocument<"iidx:DP">; + } as unknown as ChartDocument<"iidx:DP">; } - function s(lamp: Lamps["iidx:SP" | "iidx:DP"]): DryScore<"iidx:SP" | "iidx:DP"> { - return Object.assign({}, TestingIIDXSPDryScore, { + function s(lamp: Lamps["iidx:DP" | "iidx:SP"]): DryScore<"iidx:DP" | "iidx:SP"> { + return { + ...TestingIIDXSPDryScore, scoreData: { lamp, }, - }); + }; } t.equal( @@ -150,7 +155,7 @@ const logger = CreateLogCtx(__filename); t.test("#CalculateMFCP", (t) => { function TestMFCP( - lamp: Lamps["ddr:SP" | "ddr:DP"], + lamp: Lamps["ddr:DP" | "ddr:SP"], levelNum: number, difficulty: Difficulties["ddr:DP" | "ddr:SP"] ) { @@ -247,6 +252,7 @@ t.test("#CalculateMFCP", (t) => { `Should return 25 for charts with level ${i}` ); } + t.end(); }); diff --git a/server/src/lib/score-import/framework/calculated-data/stats.ts b/server/src/lib/score-import/framework/calculated-data/stats.ts index bf383f4fa..aba9c759f 100644 --- a/server/src/lib/score-import/framework/calculated-data/stats.ts +++ b/server/src/lib/score-import/framework/calculated-data/stats.ts @@ -104,36 +104,20 @@ function RatingCalcV0Fail(percentDiv100: number, levelNum: number, parameters: R } const ratingParameters = { - iidx: { - failHarshnessMultiplier: 0.3, - pivotPercent: 0.7777, // Grade: AA - clearExpMultiplier: 1, - }, - bms: { - failHarshnessMultiplier: 0.5, - pivotPercent: 0.7777, // Grade: AA - clearExpMultiplier: 0.75, - }, museca: { failHarshnessMultiplier: 1, - pivotPercent: 0.8, // grade: not fail - clearExpMultiplier: 1, // no real reason - }, - maimai: { - failHarshnessMultiplier: 1, + + // grade: not fail pivotPercent: 0.8, - clearExpMultiplier: 1, - }, - ddr: { - failHarshnessMultiplier: 0.9, - pivotPercent: 0.9, + + // no real reason clearExpMultiplier: 1, }, }; export function CalculateKTRating( dryScore: DryScore, - game: "ddr" | "museca", + game: "museca", playtype: Playtype, chart: ChartDocument, logger: KtLogger diff --git a/server/src/lib/score-import/framework/common/get-input-parser.ts b/server/src/lib/score-import/framework/common/get-input-parser.ts index a98346d0c..b1ec35ff3 100644 --- a/server/src/lib/score-import/framework/common/get-input-parser.ts +++ b/server/src/lib/score-import/framework/common/get-input-parser.ts @@ -13,6 +13,7 @@ export function GetInputParser(jobData: ScoreImportJobDat const ParserFunction = Parsers[jobData.importType]; const InputParser = (logger: KtLogger) => + // eslint-disable-next-line lines-around-comment // @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 diff --git a/server/src/lib/score-import/framework/common/score-utils.test.ts b/server/src/lib/score-import/framework/common/score-utils.test.ts index 238831cb8..8d7e1f721 100644 --- a/server/src/lib/score-import/framework/common/score-utils.test.ts +++ b/server/src/lib/score-import/framework/common/score-utils.test.ts @@ -1,8 +1,8 @@ -import { ChartDocument, Game } from "tachi-common"; +import { GenericCalculatePercent, GetGradeFromPercent, ValidatePercent } from "./score-utils"; import t from "tap"; import { isApproximately } from "test-utils/asserts"; import { Testing511SPA } from "test-utils/test-data"; -import { GenericCalculatePercent, GetGradeFromPercent, ValidatePercent } from "./score-utils"; +import type { ChartDocument, Game } from "tachi-common"; t.test("#GetGradeForPercent", (t) => { t.equal( @@ -93,24 +93,49 @@ t.test("#GenericCalculatePercent", (t) => { t.test("#ValidatePercent", (t) => { for (const playtype of ["SP", "DP"] as const) { - t.doesNotThrow(() => ValidatePercent("iidx", playtype, 90, Testing511SPA)); - t.doesNotThrow(() => ValidatePercent("iidx", playtype, 0, Testing511SPA)); - t.doesNotThrow(() => ValidatePercent("iidx", playtype, 100, Testing511SPA)); + t.doesNotThrow(() => { + ValidatePercent("iidx", playtype, 90, Testing511SPA); + }); + t.doesNotThrow(() => { + ValidatePercent("iidx", playtype, 0, Testing511SPA); + }); + t.doesNotThrow(() => { + ValidatePercent("iidx", playtype, 100, Testing511SPA); + }); - t.throws(() => ValidatePercent("iidx", playtype, -50, Testing511SPA)); - t.throws(() => ValidatePercent("iidx", playtype, 101, Testing511SPA)); + t.throws(() => { + ValidatePercent("iidx", playtype, -50, Testing511SPA); + }); + t.throws(() => { + ValidatePercent("iidx", playtype, 101, Testing511SPA); + }); } const m = (maxPercent: number) => ({ data: { maxPercent } } as ChartDocument); - t.doesNotThrow(() => ValidatePercent("maimai", "Single", 90, m(100))); - t.doesNotThrow(() => ValidatePercent("maimai", "Single", 100, m(100))); - t.doesNotThrow(() => ValidatePercent("maimai", "Single", 0, m(100))); - t.doesNotThrow(() => ValidatePercent("maimai", "Single", 110, m(120))); - t.doesNotThrow(() => ValidatePercent("maimai", "Single", 120, m(120))); - t.throws(() => ValidatePercent("maimai", "Single", 130, m(120)), { - message: /expected a number less than 120/u, + t.doesNotThrow(() => { + ValidatePercent("maimai", "Single", 90, m(100)); }); + t.doesNotThrow(() => { + ValidatePercent("maimai", "Single", 100, m(100)); + }); + t.doesNotThrow(() => { + ValidatePercent("maimai", "Single", 0, m(100)); + }); + t.doesNotThrow(() => { + ValidatePercent("maimai", "Single", 110, m(120)); + }); + t.doesNotThrow(() => { + ValidatePercent("maimai", "Single", 120, m(120)); + }); + t.throws( + () => { + ValidatePercent("maimai", "Single", 130, m(120)); + }, + { + message: /expected a number less than 120/u, + } + ); t.end(); }); diff --git a/server/src/lib/score-import/framework/common/string-asserts.test.ts b/server/src/lib/score-import/framework/common/string-asserts.test.ts index db8e7f782..fe741a05c 100644 --- a/server/src/lib/score-import/framework/common/string-asserts.test.ts +++ b/server/src/lib/score-import/framework/common/string-asserts.test.ts @@ -1,6 +1,6 @@ -import t from "tap"; import { InvalidScoreFailure } from "./converter-failures"; import { AssertStrAsPositiveInt, AssertStrAsPositiveNonZeroInt } from "./string-asserts"; +import t from "tap"; function astr(v: string) { try { @@ -78,8 +78,11 @@ t.test("#AssertStrAsPositiveInt", (t) => { t.strictSame( astr(`${Number.MAX_SAFE_INTEGER.toString()}000`), + // @warn Counterintuitive errmsg here. It is an int! - new InvalidScoreFailure(`err (Not an integer -- ${Number.MAX_SAFE_INTEGER.toString()}000.)`), + new InvalidScoreFailure( + `err (Not an integer -- ${Number.MAX_SAFE_INTEGER.toString()}000.)` + ), "Should reject numbers > max_safe_integer." ); @@ -152,8 +155,11 @@ t.test("#AssertStrAsPositiveNonZeroInt", (t) => { t.strictSame( astrp(`${Number.MAX_SAFE_INTEGER.toString()}000`), + // @warn Counterintuitive errmsg here. It is an int! - new InvalidScoreFailure(`err (Not an integer -- ${Number.MAX_SAFE_INTEGER.toString()}000.)`), + new InvalidScoreFailure( + `err (Not an integer -- ${Number.MAX_SAFE_INTEGER.toString()}000.)` + ), "Should reject numbers > max_safe_integer." ); @@ -162,7 +168,6 @@ t.test("#AssertStrAsPositiveNonZeroInt", (t) => { new InvalidScoreFailure(`err (Was negative or zero -- -1.)`), "Should reject negative integer input" ); -1 t.strictSame( astrp("-0"), new InvalidScoreFailure(`err (Was negative or zero -- 0.)`), diff --git a/server/src/lib/score-import/framework/common/types.ts b/server/src/lib/score-import/framework/common/types.ts index 746400ae4..cd722594d 100644 --- a/server/src/lib/score-import/framework/common/types.ts +++ b/server/src/lib/score-import/framework/common/types.ts @@ -1,5 +1,4 @@ -import { Game } from "tachi-common"; -import type { IDStrings, ScoreDocument, Playtype } from "tachi-common"; +import type { IDStrings, Playtype, ScoreDocument } from "tachi-common"; /** * An intermediate score format that will be filled out by diff --git a/server/src/lib/score-import/framework/goals/goals.test.ts b/server/src/lib/score-import/framework/goals/goals.test.ts index c886723c0..1e5bf5863 100644 --- a/server/src/lib/score-import/framework/goals/goals.test.ts +++ b/server/src/lib/score-import/framework/goals/goals.test.ts @@ -1,9 +1,9 @@ -import t from "tap"; -import db from "external/mongo/db"; -import ResetDBState from "test-utils/resets"; import { GetRelevantFolderGoals, GetRelevantGoals, UpdateGoalsForUser, ProcessGoal } from "./goals"; -import { ChartDocument, GoalDocument, GoalSubscriptionDocument, SongDocument } from "tachi-common"; -import { CreateFolderChartLookup } from "utils/folder"; +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 { GetKTDataJSON, Testing511SPA, @@ -12,9 +12,14 @@ import { HC511UserGoal, TestingIIDXFolderSP10, } from "test-utils/test-data"; -import deepmerge from "deepmerge"; -import CreateLogCtx from "lib/logger/logger"; +import { CreateFolderChartLookup } from "utils/folder"; import crypto from "crypto"; +import type { + ChartDocument, + GoalDocument, + GoalSubscriptionDocument, + SongDocument, +} from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -61,6 +66,7 @@ t.test("#GetRelevantFolderGoals", (t) => { data: { level: "11" }, folderID: "foo", }); + await db.folders.insert(TestingIIDXFolderSP10); await db.folders.insert(sp11folder); await db.goals.insert(fakeFolderGoalDocument); @@ -93,11 +99,17 @@ t.test("#GetRelevantGoals", (t) => { // use the real data so we have enough charts loaded for this to test properly. await db.songs.iidx.remove({}); await db.charts.iidx.remove({}); - await db.charts.iidx.insert(GetKTDataJSON("./tachi/tachi-charts-iidx.json") as ChartDocument<"iidx:SP"|"iidx:DP">[]); - await db.songs.iidx.insert(GetKTDataJSON("./tachi/tachi-songs-iidx.json") as SongDocument<"iidx">[]); + await db.charts.iidx.insert( + GetKTDataJSON("./tachi/tachi-charts-iidx.json") as Array< + ChartDocument<"iidx:DP" | "iidx:SP"> + > + ); + await db.songs.iidx.insert( + GetKTDataJSON("./tachi/tachi-songs-iidx.json") as Array> + ); const lotsOfCharts = await db.charts.iidx.find({}, { limit: 20 }); - const goals: GoalDocument[] = lotsOfCharts.map((e) => ({ + const goals: Array = lotsOfCharts.map((e) => ({ charts: { type: "single", data: e.chartID, @@ -200,6 +212,7 @@ t.test("#UpdateGoalsForUser", (t) => { delete baseGoalDocument._id; await db["goal-subs"].insert(baseGoalSubscriptionDocument); + // we dont delete _id here because updategoalsforuser // depends on usergoal _id @@ -251,6 +264,7 @@ t.test("#UpdateGoalsForUser", (t) => { t.test("Should correctly update goals when user does not achieve goal.", async (t) => { const goal = deepmerge(baseGoalDocument, { criteria: { value: 2 } }); + await db.goals.insert(goal); const goalSub = deepmerge(baseGoalSubscriptionDocument, { @@ -259,6 +273,7 @@ t.test("#UpdateGoalsForUser", (t) => { }) as unknown as GoalSubscriptionDocument; await db["goal-subs"].insert(goalSub); + // we dont delete _id here because updategoalsforuser // depends on usergoal _id @@ -355,7 +370,9 @@ t.test("#ProcessGoal", (t) => { t.test("Should process the users goal if a score has changed.", async (t) => { await db["goal-subs"].insert(HC511UserGoal); - await db["personal-bests"].insert(TestingIIDXSPScorePB); // score is EX HARD CLEAR by default. + + // score is EX HARD CLEAR by default. + await db["personal-bests"].insert(TestingIIDXSPScorePB); const res = await ProcessGoal(HC511Goal, HC511UserGoal, 1, logger); diff --git a/server/src/lib/score-import/framework/goals/goals.ts b/server/src/lib/score-import/framework/goals/goals.ts index 4ebbd5f9f..a7866111c 100644 --- a/server/src/lib/score-import/framework/goals/goals.ts +++ b/server/src/lib/score-import/framework/goals/goals.ts @@ -143,7 +143,7 @@ export async function ProcessGoal( // that haven't changed return nothing instead of // getting to this point. lastInteraction: Date.now(), - } as Partial; + } as unknown as Partial; // If this goal was achieved, and is now *not* achieved, we need to unset // some things. @@ -262,7 +262,7 @@ export function GetRelevantFolderGoals(goalIDs: Array, chartIDsArr: Arra // it's weird to do this in mongodb, but this seems like the right // way to actually handle this. - return db.goals.aggregate([ + const result: Promise> = db.goals.aggregate([ { $match: { "charts.type": "folder", @@ -288,4 +288,6 @@ export function GetRelevantFolderGoals(goalIDs: Array, chartIDsArr: Arra }, }, ]); + + return result; } diff --git a/server/src/lib/score-import/framework/milestones/milestones.test.ts b/server/src/lib/score-import/framework/milestones/milestones.test.ts index 0e49b7d8b..317d9815d 100644 --- a/server/src/lib/score-import/framework/milestones/milestones.test.ts +++ b/server/src/lib/score-import/framework/milestones/milestones.test.ts @@ -1,20 +1,20 @@ +import { UpdateUsersMilestones } from "./milestones"; import deepmerge from "deepmerge"; import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; -import { GoalImportInfo } from "tachi-common"; import t from "tap"; import ResetDBState from "test-utils/resets"; import { TestingIIDXSPMilestone } from "test-utils/test-data"; -import { UpdateUsersMilestones } from "./milestones"; +import type { GoalImportInfo } from "tachi-common"; const logger = CreateLogCtx(__filename); -function CreateMockGII(...garr: [string, boolean][]) { +function CreateMockGII(...garr: Array<[string, boolean]>) { return garr.map((e) => ({ goalID: e[0], old: {}, new: { achieved: e[1] }, - })) as unknown as GoalImportInfo[]; + })) as unknown as Array; } t.test("#UpdateUsersMilestones", (t) => { @@ -149,6 +149,7 @@ t.test("#UpdateUsersMilestones", (t) => { t.test("Test with new goals on multiple milestones.", async (t) => { delete TestingIIDXSPMilestone._id; await db.milestones.insert( + // eslint-disable-next-line lines-around-comment // @ts-expect-error lol deepmerge(TestingIIDXSPMilestone, { milestoneID: "some_other_milestone_with_mutual_goals", diff --git a/server/src/lib/score-import/framework/milestones/milestones.ts b/server/src/lib/score-import/framework/milestones/milestones.ts index 5b63c464e..1982f64f8 100644 --- a/server/src/lib/score-import/framework/milestones/milestones.ts +++ b/server/src/lib/score-import/framework/milestones/milestones.ts @@ -11,6 +11,7 @@ import type { MilestoneImportInfo, Playtype, MilestoneSubscriptionDocument, + GoalImportStat, } from "tachi-common"; /** @@ -75,13 +76,13 @@ export async function UpdateUsersMilestones( // create a map here to avoid linear searching when // co-iterating - const milestoneSubMap = new Map(); + const milestoneSubMap = new Map(); for (const um of milestoneSubs) { milestoneSubMap.set(um.milestoneID, um); } - const importGoalMap = new Map(); + const importGoalMap = new Map(); for (const ig of importGoalInfo) { importGoalMap.set(ig.goalID, ig.new); @@ -138,7 +139,7 @@ export async function UpdateUsersMilestones( bwrite.push(bwriteOp); if (achieved && !milestoneSub.achieved) { - EmitWebhookEvent({ + void EmitWebhookEvent({ type: "milestone-achieved/v1", content: { userID, diff --git a/server/src/lib/score-import/framework/orphans/orphans.test.ts b/server/src/lib/score-import/framework/orphans/orphans.test.ts index 9f65fb37d..5bcc74ef5 100644 --- a/server/src/lib/score-import/framework/orphans/orphans.test.ts +++ b/server/src/lib/score-import/framework/orphans/orphans.test.ts @@ -1,14 +1,13 @@ -import t from "tap"; -import db from "external/mongo/db"; - -import ResetDBState from "test-utils/resets"; -import CreateLogCtx from "lib/logger/logger"; -import { BatchManualContext } from "../../import-types/common/batch-manual/types"; import { OrphanScore, ReprocessOrphan } from "./orphans"; -import fjsh from "fast-json-stable-hash"; -import { OrphanScoreDocument } from "../../import-types/common/types"; import deepmerge from "deepmerge"; -import { BatchManualScore } from "tachi-common"; +import db from "external/mongo/db"; +import fjsh from "fast-json-stable-hash"; +import CreateLogCtx from "lib/logger/logger"; +import t from "tap"; +import ResetDBState from "test-utils/resets"; +import type { BatchManualContext } from "../../import-types/common/batch-manual/types"; +import type { OrphanScoreDocument } from "../../import-types/common/types"; +import type { BatchManualScore } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -133,7 +132,8 @@ t.test("#ReprocessOrphan", (t) => { { success: true, type: "ScoreImported", - message: "Imported score R7a3a2b04bd4882ec06c198d78297fe3d56561502c7b134c067214bbfdf4f1602.", + message: + "Imported score R7a3a2b04bd4882ec06c198d78297fe3d56561502c7b134c067214bbfdf4f1602.", content: { score: { game: "iidx", diff --git a/server/src/lib/score-import/framework/orphans/orphans.ts b/server/src/lib/score-import/framework/orphans/orphans.ts index 2b3c04731..7c8071233 100644 --- a/server/src/lib/score-import/framework/orphans/orphans.ts +++ b/server/src/lib/score-import/framework/orphans/orphans.ts @@ -103,7 +103,9 @@ export async function ReprocessOrphan( logger.error(`Converter function ${orphan.importType} returned unexpected error.`, { err, }); - throw err; // throw this higher up, i guess. + + // throw this higher up, i guess. + throw err; } res = err; diff --git a/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts b/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts index 83133d806..69765301a 100644 --- a/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts +++ b/server/src/lib/score-import/framework/pb/create-pb-doc.test.ts @@ -1,11 +1,12 @@ +import { CreatePBDoc } from "./create-pb-doc"; import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import CreateLogCtx, { KtLogger } from "lib/logger/logger"; +import CreateLogCtx from "lib/logger/logger"; import { GetGamePTConfig } from "tachi-common"; import t from "tap"; import ResetDBState from "test-utils/resets"; import { Testing511SPA, TestingBMS7KScore, TestingIIDXSPScore } from "test-utils/test-data"; -import { CreatePBDoc } from "./create-pb-doc"; +import type { KtLogger } from "lib/logger/logger"; const IIDXScore = TestingIIDXSPScore; @@ -22,6 +23,7 @@ t.test("#CreatePBDoc", (t) => { chartID, userID: 1, songID: 1, + // rankingData -- is not present because it is not added until post-processing. highlight: false, isPrimary: true, diff --git a/server/src/lib/score-import/framework/pb/create-pb-doc.ts b/server/src/lib/score-import/framework/pb/create-pb-doc.ts index 0d3986bd5..3d186d906 100644 --- a/server/src/lib/score-import/framework/pb/create-pb-doc.ts +++ b/server/src/lib/score-import/framework/pb/create-pb-doc.ts @@ -8,7 +8,7 @@ import { import db from "external/mongo/db"; import type { KtLogger } from "lib/logger/logger"; import type { BulkWriteUpdateOneOperation } from "mongodb"; -import type { integer, PBScoreDocument, ScoreDocument } from "tachi-common"; +import type { Game, integer, PBScoreDocument, ScoreDocument } from "tachi-common"; export type PBScoreDocumentNoRank = Omit; @@ -108,18 +108,6 @@ export async function UpdateChartRanking(chartID: string) { await db["personal-bests"].bulkWrite(bwrite, { ordered: false }); } -// Explicit acknowledgement that typing this properly simply takes too much time -// This is a function that is aptly described below when you see how its called. -// They return true on success, false on failure, and mutate their arguments. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const GAME_SPECIFIC_MERGE_FNS: Record = { - iidx: IIDXMergeFn, - sdvx: SDVXMergeFn, - usc: USCMergeFn, - popn: PopnMergeFn, - bms: BMSMergeFn, -}; - async function MergeScoreLampIntoPB( userID: integer, scorePB: ScoreDocument, @@ -167,18 +155,38 @@ async function MergeScoreLampIntoPB( calculatedData: scorePB.calculatedData, }; - const GameSpecificMergeFn = GAME_SPECIFIC_MERGE_FNS[scorePB.game]; + const GameSpecificMergeFn = GetGameSpecificMergeFn(scorePB.game); if (GameSpecificMergeFn) { + // @ts-expect-error Yeah, this call sucks. It correctly warns us that scorePB and lampPB + // might've diverged, but we know they haven't. const success = await GameSpecificMergeFn(pbDoc, scorePB, lampPB, logger); // If the mergeFn returns false, this means something has gone // rather wrong. We just return undefined here, which in turn // tells our calling code to skip this PB entirely. - if (success === false) { + if (!success) { return; } } return pbDoc; } + +function GetGameSpecificMergeFn(game: Game) { + switch (game) { + case "iidx": + return IIDXMergeFn; + case "usc": + return USCMergeFn; + case "sdvx": + return SDVXMergeFn; + case "popn": + return PopnMergeFn; + case "bms": + case "pms": + return BMSMergeFn; + default: + return null; + } +} diff --git a/server/src/lib/score-import/framework/pb/game-specific-merge.ts b/server/src/lib/score-import/framework/pb/game-specific-merge.ts index b657a3f7a..d23f4a4fc 100644 --- a/server/src/lib/score-import/framework/pb/game-specific-merge.ts +++ b/server/src/lib/score-import/framework/pb/game-specific-merge.ts @@ -23,10 +23,11 @@ export async function IIDXMergeFn( }, { sort: { - "scoreData.hitMeta.bp": 1, // bp 0 is the best BP, bp 1 is worse, so on + // bp 0 is the best BP, bp 1 is worse, so on + "scoreData.hitMeta.bp": 1, }, } - )) as ScoreDocument<"iidx:DP" | "iidx:SP">; + )) as ScoreDocument<"iidx:DP" | "iidx:SP"> | null; if (!bpPB) { logger.verbose( @@ -64,7 +65,7 @@ export function PopnMergeFn( pbDoc: PBScoreDocument<"popn:9B">, scorePB: ScoreDocument<"popn:9B">, lampPB: ScoreDocument<"popn:9B">, - logger: KtLogger + _logger: KtLogger ) { pbDoc.scoreData.hitMeta.specificClearType = lampPB.scoreData.hitMeta.specificClearType; @@ -75,7 +76,7 @@ export function BMSMergeFn( pbDoc: PBScoreDocument<"bms:7K" | "bms:14K">, scorePB: ScoreDocument<"bms:7K" | "bms:14K">, lampPB: ScoreDocument<"bms:7K" | "bms:14K">, - logger: KtLogger + _logger: KtLogger ) { pbDoc.calculatedData.sieglinde = lampPB.calculatedData.sieglinde; diff --git a/server/src/lib/score-import/framework/pb/process-pbs.test.ts b/server/src/lib/score-import/framework/pb/process-pbs.test.ts index 545b3f988..2167591f7 100644 --- a/server/src/lib/score-import/framework/pb/process-pbs.test.ts +++ b/server/src/lib/score-import/framework/pb/process-pbs.test.ts @@ -1,11 +1,11 @@ -import crypto from "crypto"; +import { ProcessPBs } from "./process-pbs"; 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 { Testing511SPA, TestingIIDXSPScore } from "test-utils/test-data"; -import { ProcessPBs } from "./process-pbs"; +import crypto from "crypto"; const logger = CreateLogCtx(__filename); @@ -34,11 +34,13 @@ t.test("#ProcessPBs", (t) => { chartID: "test1", scoreID: crypto.randomBytes(20).toString("hex"), }), + // @ts-expect-error lol deepmerge(TestingIIDXSPScore, { chartID: "test2", scoreID: crypto.randomBytes(20).toString("hex"), }), + // @ts-expect-error lol deepmerge(TestingIIDXSPScore, { chartID: "test3", diff --git a/server/src/lib/score-import/framework/score-import.ts b/server/src/lib/score-import/framework/score-import.ts index 9e5c3f9fd..141d0461d 100644 --- a/server/src/lib/score-import/framework/score-import.ts +++ b/server/src/lib/score-import/framework/score-import.ts @@ -7,7 +7,7 @@ import { JOB_RETRY_COUNT } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { ServerConfig } from "lib/setup/config"; import { Sleep } from "utils/misc"; -import type { ScoreImportJobData } from "../worker/types"; +import type { ScoreImportJobData, ScoreImportWorkerReturns } from "../worker/types"; import type { ImportDocument, ImportTypes, integer } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -40,7 +40,9 @@ export async function MakeScoreImport( } ); - const data = await job.waitUntilFinished(ScoreImportQueueEvents); + const data = (await job.waitUntilFinished( + ScoreImportQueueEvents + )) as ScoreImportWorkerReturns; if (data.success) { return data.importDocument; diff --git a/server/src/lib/score-import/framework/score-importing/insert-score.test.ts b/server/src/lib/score-import/framework/score-importing/insert-score.test.ts index 523edd516..4b2f6062a 100644 --- a/server/src/lib/score-import/framework/score-importing/insert-score.test.ts +++ b/server/src/lib/score-import/framework/score-importing/insert-score.test.ts @@ -1,9 +1,8 @@ -import { ScoreDocument } from "tachi-common"; -import t from "tap"; -import db from "external/mongo/db"; - -import ResetDBState from "test-utils/resets"; import { InsertQueue, QueueScoreInsert } from "./insert-score"; +import db from "external/mongo/db"; +import t from "tap"; +import ResetDBState from "test-utils/resets"; +import type { ScoreDocument } from "tachi-common"; // these two get the same tests, because they're too closely linked t.test("#QueueScoreInsert, #InsertQueue", async (t) => { @@ -45,7 +44,8 @@ t.test("#QueueScoreInsert, #InsertQueue", async (t) => { t.end(); }); - let r = await InsertQueue(1); // flush queue just incase former test fails. + // flush queue just incase former test fails. + let r = await InsertQueue(1); t.test("Queue Overflow Test", async (t) => { for (let i = 0; i < 499; i++) { @@ -87,7 +87,8 @@ t.test("#QueueScoreInsert, #InsertQueue", async (t) => { t.end(); }); - r = await InsertQueue(1); // flush queue just incase former test fails. + // flush queue just incase former test fails. + r = await InsertQueue(1); t.equal(r, 0, "Queue should be empty after test."); @@ -129,9 +130,11 @@ t.test("#QueueScoreInsert, #InsertQueue", async (t) => { } as unknown as ScoreDocument); const r1 = await InsertQueue(1); + t.equal(r1, 1, "Queue for userID 1 should have length 1."); const r2 = await InsertQueue(2); + t.equal(r2, 1, "Queue for userID 2 should also have length 1."); t.end(); diff --git a/server/src/lib/score-import/framework/score-importing/insert-score.ts b/server/src/lib/score-import/framework/score-importing/insert-score.ts index 10c653d23..a3ccc0e6a 100644 --- a/server/src/lib/score-import/framework/score-importing/insert-score.ts +++ b/server/src/lib/score-import/framework/score-importing/insert-score.ts @@ -59,13 +59,16 @@ export async function InsertQueue(userID: integer) { const queuedScores = scoreQueue.queue.splice(0); if (queuedScores.length !== 0) { + // Yeah, this is sketchy. Ah well. + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete ScoreQueues[userID]; try { await db.scores.insert(queuedScores); } catch (err) { logger.warn( - `Triggered duplicate key protection. Race condition protected against, but this is not good.` + `Triggered duplicate key protection. Race condition protected against, but this is not good.`, + { err } ); return null; } diff --git a/server/src/lib/score-import/framework/score-importing/score-id.test.ts b/server/src/lib/score-import/framework/score-importing/score-id.test.ts index fc0fb57ad..306c9cef8 100644 --- a/server/src/lib/score-import/framework/score-importing/score-id.test.ts +++ b/server/src/lib/score-import/framework/score-importing/score-id.test.ts @@ -1,9 +1,8 @@ -import { ScoreDocument } from "tachi-common"; +import { CreateScoreID, GetWithScoreID } from "./score-id"; import t from "tap"; - import ResetDBState from "test-utils/resets"; import { Testing511SPA, TestingIIDXSPDryScore } from "test-utils/test-data"; -import { CreateScoreID, GetWithScoreID } from "./score-id"; +import type { ScoreDocument } from "tachi-common"; t.test("#GetWithScoreID", async (t) => { await ResetDBState(); diff --git a/server/src/lib/score-import/framework/score-importing/score-import-error.test.ts b/server/src/lib/score-import/framework/score-importing/score-import-error.test.ts index 9f5123004..64327ce1d 100644 --- a/server/src/lib/score-import/framework/score-importing/score-import-error.test.ts +++ b/server/src/lib/score-import/framework/score-importing/score-import-error.test.ts @@ -1,5 +1,5 @@ -import t from "tap"; import ScoreImportFatalError from "./score-import-error"; +import t from "tap"; t.test("#new ScoreImportFatalError()", (t) => { const err = new ScoreImportFatalError(500, "error message"); diff --git a/server/src/lib/score-import/framework/score-importing/score-import-main.ts b/server/src/lib/score-import/framework/score-importing/score-import-main.ts index 0c55d071d..819e3a835 100644 --- a/server/src/lib/score-import/framework/score-importing/score-import-main.ts +++ b/server/src/lib/score-import/framework/score-importing/score-import-main.ts @@ -401,7 +401,7 @@ async function UpdateUsersGameStats( * on their playtype. */ function ParseImportInfo(importInfo: Array) { - const scorePlaytypeMap: ScorePlaytypeMap = Object.create(null); + const scorePlaytypeMap: ScorePlaytypeMap = {}; const scoreIDs = []; const errors = []; diff --git a/server/src/lib/score-import/framework/score-importing/score-importing.ts b/server/src/lib/score-import/framework/score-importing/score-importing.ts index d570183c1..ed7f9f346 100644 --- a/server/src/lib/score-import/framework/score-importing/score-importing.ts +++ b/server/src/lib/score-import/framework/score-importing/score-importing.ts @@ -305,8 +305,6 @@ async function HydrateAndInsertScore( // micro-optimisation - mongoDB is significantly faster when returning less fields // since we only care about whether we have a score or not here, we can minimise returned // fields. - // - // feel free to test this yourself! - zkldi (09/04/2021) projection: { _id: 1, }, @@ -319,7 +317,7 @@ async function HydrateAndInsertScore( } // If this users score queue - if (GetScoreQueueMaybe(userID)?.scoreIDSet.has(scoreID)) { + if (GetScoreQueueMaybe(userID)?.scoreIDSet.has(scoreID) === true) { logger.verbose(`Skipped score.`); return null; } diff --git a/server/src/lib/score-import/framework/sessions/calculated-data.test.ts b/server/src/lib/score-import/framework/sessions/calculated-data.test.ts index 23afbf26a..375488ffc 100644 --- a/server/src/lib/score-import/framework/sessions/calculated-data.test.ts +++ b/server/src/lib/score-import/framework/sessions/calculated-data.test.ts @@ -1,7 +1,7 @@ -import t from "tap"; import { CreateSessionCalcData } from "./calculated-data"; +import t from "tap"; -function ratingwrap(ratings: [number, number][]) { +function ratingwrap(ratings: Array<[number, number]>) { return ratings.map((e) => ({ calculatedData: { BPI: e[0], @@ -10,7 +10,7 @@ function ratingwrap(ratings: [number, number][]) { })); } -function avgbest10(arr: number[]) { +function avgbest10(arr: Array) { return ( arr .sort((a, b) => b - a) diff --git a/server/src/lib/score-import/framework/sessions/calculated-data.ts b/server/src/lib/score-import/framework/sessions/calculated-data.ts index 5dc4ee5aa..98731c01d 100644 --- a/server/src/lib/score-import/framework/sessions/calculated-data.ts +++ b/server/src/lib/score-import/framework/sessions/calculated-data.ts @@ -1,4 +1,11 @@ -import type { Game, Playtype, Playtypes, ScoreDocument, SessionDocument } from "tachi-common"; +import type { + Game, + IDStrings, + Playtype, + ScoreDocument, + SessionCalculatedDataLookup, + SessionDocument, +} from "tachi-common"; type ScoreCalculatedDataOnly = Pick; @@ -36,122 +43,79 @@ function AvgBest10Map( return AverageBest10(arr.map((e) => e.calculatedData[prop])); } -type CalculatedDataFunctions = { - [G in Game]: { - [P in Playtypes[G]]: ( - scoreCalcData: Array - ) => SessionDocument["calculatedData"]; - }; -}; +type SessionCalcDataFn = ( + scd: Array +) => Partial>; -const CalculatedDataFunctions: CalculatedDataFunctions = { - iidx: { - SP: (scd) => ({ - BPI: AvgBest10Map(scd, "BPI"), - ktLampRating: AvgBest10Map(scd, "ktLampRating"), - }), - DP: (scd) => ({ - BPI: AvgBest10Map(scd, "BPI"), - ktLampRating: AvgBest10Map(scd, "ktLampRating"), - }), - }, - sdvx: { - Single: (scd) => { - const VF6 = AvgBest10Map(scd, "VF6"); +function GetGPTSessionCalcDataFn(game: Game, playtype: Playtype): SessionCalcDataFn { + switch (`${game}:${playtype}` as IDStrings) { + case "iidx:SP": + case "iidx:DP": + return (scd) => ({ + BPI: AvgBest10Map(scd, "BPI"), + ktLampRating: AvgBest10Map(scd, "ktLampRating"), + }); + case "sdvx:Single": + case "usc:Controller": + case "usc:Keyboard": + return (scd) => { + const VF6 = AvgBest10Map(scd, "VF6"); - return { - VF6, - ProfileVF6: VF6 === null ? null : VF6 * 50, + return { + VF6, + ProfileVF6: VF6 === null ? null : VF6 * 50, + }; }; - }, - }, - popn: { - "9B": (scd) => ({ - classPoints: AvgBest10Map(scd, "classPoints"), - }), - }, - museca: { - Single: (scd) => ({ - ktRating: AvgBest10Map(scd, "ktRating"), - }), - }, - chunithm: { - Single: (scd) => ({ - naiveRating: AvgBest10Map(scd, "rating"), - }), - }, - maimai: { - Single: () => ({}), - }, - gitadora: { - Gita: (scd) => ({ - skill: AvgBest10Map(scd, "skill"), - }), - Dora: (scd) => ({ - skill: AvgBest10Map(scd, "skill"), - }), - }, - bms: { - "7K": (scd) => ({ - sieglinde: AvgBest10Map(scd, "sieglinde"), - }), - "14K": (scd) => ({ - sieglinde: AvgBest10Map(scd, "sieglinde"), - }), - }, - pms: { - Controller: (scd) => ({ - sieglinde: AvgBest10Map(scd, "sieglinde"), - }), - Keyboard: (scd) => ({ - sieglinde: AvgBest10Map(scd, "sieglinde"), - }), - }, - ddr: { - SP: (scd) => ({ - MFCP: SumAll(scd, "MFCP"), - ktRating: AvgBest10Map(scd, "ktRating"), - }), - DP: (scd) => ({ - MFCP: SumAll(scd, "MFCP"), - ktRating: AvgBest10Map(scd, "ktRating"), - }), - }, - jubeat: { - Single: (scd) => ({ - jubility: AvgBest10Map(scd, "jubility"), - }), - }, - usc: { - Controller: (scd) => { - const VF6 = AvgBest10Map(scd, "VF6"); - return { - VF6, - ProfileVF6: VF6 === null ? null : VF6 * 50, - }; - }, - Keyboard: (scd) => { - const VF6 = AvgBest10Map(scd, "VF6"); - - return { - VF6, - ProfileVF6: VF6 === null ? null : VF6 * 50, - }; - }, - }, - wacca: { - Single: (scd) => ({ - rate: AvgBest10Map(scd, "rate"), - }), - }, -}; + case "popn:9B": + return (scd) => ({ + classPoints: AvgBest10Map(scd, "classPoints"), + }); + case "museca:Single": + return (scd) => ({ + naiveRating: AvgBest10Map(scd, "rating"), + }); + case "gitadora:Dora": + case "gitadora:Gita": + return (scd) => ({ + skill: AvgBest10Map(scd, "skill"), + }); + case "bms:7K": + case "bms:14K": + case "pms:Controller": + case "pms:Keyboard": + return (scd) => ({ + sieglinde: AvgBest10Map(scd, "sieglinde"), + }); + case "ddr:SP": + case "ddr:DP": + return (scd) => ({ + MFCP: SumAll(scd, "MFCP"), + ktRating: AvgBest10Map(scd, "ktRating"), + }); + case "jubeat:Single": + return (scd) => ({ + jubility: AvgBest10Map(scd, "jubility"), + }); + case "wacca:Single": + return (scd) => ({ + rate: AvgBest10Map(scd, "rate"), + }); + case "chunithm:Single": + return (scd) => ({ + naiveRating: AvgBest10Map(scd, "rating"), + }); + case "maimai:Single": + return () => ({}); + } +} export function CreateSessionCalcData( game: Game, playtype: Playtype, scoreCalcData: Array ): SessionDocument["calculatedData"] { - // @ts-expect-error standard game->pt stuff. - return CalculatedDataFunctions[game][playtype](scoreCalcData); + const SessionCalculatedDataFunction = GetGPTSessionCalcDataFn(game, playtype); + + return SessionCalculatedDataFunction(scoreCalcData); } diff --git a/server/src/lib/score-import/framework/sessions/name-generation.test.ts b/server/src/lib/score-import/framework/sessions/name-generation.test.ts index 89e103745..28ec06a3c 100644 --- a/server/src/lib/score-import/framework/sessions/name-generation.test.ts +++ b/server/src/lib/score-import/framework/sessions/name-generation.test.ts @@ -1,5 +1,5 @@ -import t from "tap"; import { GenerateRandomSessionName } from "./name-generation"; +import t from "tap"; t.test("#GenerateRandomSessionName", (t) => { const res = GenerateRandomSessionName(); diff --git a/server/src/lib/score-import/framework/sessions/sessions.test.ts b/server/src/lib/score-import/framework/sessions/sessions.test.ts index 9f3e04d01..8d3569f10 100644 --- a/server/src/lib/score-import/framework/sessions/sessions.test.ts +++ b/server/src/lib/score-import/framework/sessions/sessions.test.ts @@ -1,11 +1,11 @@ +import { CreateSessions, LoadScoresIntoSessions } from "./sessions"; +import { CreateScoreLogger } from "../common/import-logger"; import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import { PublicUserDocument, ScoreDocument, SessionDocument } from "tachi-common"; import t from "tap"; import ResetDBState from "test-utils/resets"; import { TestingIIDXSPScore, TestingIIDXSPScorePB } from "test-utils/test-data"; -import { CreateScoreLogger } from "../common/import-logger"; -import { CreateSessions, LoadScoresIntoSessions } from "./sessions"; +import type { PublicUserDocument, ScoreDocument, SessionDocument } from "tachi-common"; const logger = CreateScoreLogger( { username: "test_zkldi", id: 1 } as PublicUserDocument, @@ -44,6 +44,7 @@ t.test("#CreateSessions", (t) => { t.hasStrict(session, { userID: 1, importType: "ir/direct-manual", + // name: "adjective1 adjective2 noun1", sessionID: res[0]?.sessionID, desc: null, @@ -56,6 +57,7 @@ t.test("#CreateSessions", (t) => { isNewScore: true, }, ], + // timeInserted: 1622289329729, timeStarted: 1619454485988, timeEnded: 1619454485988, @@ -120,7 +122,7 @@ t.test("#LoadScoresIntoSessions", (t) => { "ir/direct-manual", [null, start, start + 1000, start + 2000, start + 3000].map((e, i) => deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) - ) as ScoreDocument[], + ) as Array, "iidx", "SP", logger @@ -166,7 +168,7 @@ t.test("#LoadScoresIntoSessions", (t) => { [null, start, start - 4000, start + 2000, start - 3000, null, start + 1000].map( (e, i) => deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) - ) as ScoreDocument[], + ) as Array, "iidx", "SP", logger @@ -225,7 +227,7 @@ t.test("#LoadScoresIntoSessions", (t) => { start + TWO_HOURS + 6000, ].map((e, i) => deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) - ) as ScoreDocument[], + ) as Array, "iidx", "SP", logger @@ -300,7 +302,7 @@ t.test("#LoadScoresIntoSessions", (t) => { "ir/direct-manual", [null, start, start + 1000, start + 2000].map((e, i) => deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) - ) as ScoreDocument[], + ) as Array, "iidx", "SP", logger @@ -363,7 +365,7 @@ t.test("#LoadScoresIntoSessions", (t) => { "ir/direct-manual", [null, start, start - 1000, start - 2000].map((e, i) => deepmerge(TestingIIDXSPScore, { timeAchieved: e, scoreID: `SCORE_ID_${i}` }) - ) as ScoreDocument[], + ) as Array, "iidx", "SP", logger diff --git a/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.test.ts b/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.test.ts index 72c6f943e..02492b806 100644 --- a/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.test.ts +++ b/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.test.ts @@ -1,12 +1,12 @@ -import t from "tap"; -import { GitadoraColours, SDVXVFClasses } from "lib/constants/classes"; -import CreateLogCtx from "lib/logger/logger"; import { CalculateGitadoraColour, GitadoraSkillToColour, CalculateSDVXClass, SDVXVF6ToClass, } from "./builtin-class-handlers"; +import { GitadoraColours, SDVXVFClasses } from "lib/constants/classes"; +import CreateLogCtx from "lib/logger/logger"; +import t from "tap"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/framework/user-game-stats/classes.test.ts b/server/src/lib/score-import/framework/user-game-stats/classes.test.ts index 24943e0bc..d27ce5947 100644 --- a/server/src/lib/score-import/framework/user-game-stats/classes.test.ts +++ b/server/src/lib/score-import/framework/user-game-stats/classes.test.ts @@ -1,10 +1,9 @@ -import { UserGameStats } from "tachi-common"; -import t from "tap"; -import CreateLogCtx from "lib/logger/logger"; -import ResetDBState from "test-utils/resets"; import { ProcessClassDeltas, UpdateUGSClasses } from "./classes"; - import { GitadoraColours } from "lib/constants/classes"; +import CreateLogCtx from "lib/logger/logger"; +import t from "tap"; +import ResetDBState from "test-utils/resets"; +import type { UserGameStats } from "tachi-common"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/framework/user-game-stats/classes.ts b/server/src/lib/score-import/framework/user-game-stats/classes.ts index 6e80eadfb..74f6b6694 100644 --- a/server/src/lib/score-import/framework/user-game-stats/classes.ts +++ b/server/src/lib/score-import/framework/user-game-stats/classes.ts @@ -101,8 +101,9 @@ export async function UpdateUGSClasses( let classes: ScoreClasses = {}; // @ts-expect-error This one sucks - I need to look into a better way of representing these types - if (STATIC_CLASS_HANDLERS[game] && STATIC_CLASS_HANDLERS[game][playtype]) { + if (STATIC_CLASS_HANDLERS[game]?.[playtype] !== undefined) { // @ts-expect-error see above + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call classes = await STATIC_CLASS_HANDLERS[game][playtype]( game, playtype, @@ -141,7 +142,7 @@ export async function ProcessClassDeltas( const achievementOps = []; - for (const s in classes) { + for (const s of Object.keys(classes)) { const classSet = s as keyof GameClasses; const classVal = classes[classSet]; @@ -176,7 +177,7 @@ export async function ProcessClassDeltas( }; } - EmitWebhookEvent({ type: "class-update/v1", content: { userID, ...delta } }); + void EmitWebhookEvent({ type: "class-update/v1", content: { userID, ...delta } }); achievementOps.push({ userID, diff --git a/server/src/lib/score-import/framework/user-game-stats/rating.test.ts b/server/src/lib/score-import/framework/user-game-stats/rating.test.ts index a675c6964..c7c3b941d 100644 --- a/server/src/lib/score-import/framework/user-game-stats/rating.test.ts +++ b/server/src/lib/score-import/framework/user-game-stats/rating.test.ts @@ -1,7 +1,7 @@ +import { CalculateRatings } from "./rating"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import ResetDBState from "test-utils/resets"; -import { CalculateRatings } from "./rating"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/framework/user-game-stats/rating.ts b/server/src/lib/score-import/framework/user-game-stats/rating.ts index 4318dd362..d722172d5 100644 --- a/server/src/lib/score-import/framework/user-game-stats/rating.ts +++ b/server/src/lib/score-import/framework/user-game-stats/rating.ts @@ -6,7 +6,6 @@ import type { integer, PBScoreDocument, Playtype, - Playtypes, ScoreCalculatedDataLookup, UserGameStats, } from "tachi-common"; @@ -88,119 +87,84 @@ const LazySumN = (key: CustomCalcNames, n: integer, nullIfNotEnoughScores = fals const LazyMeanN = (key: CustomCalcNames, n: integer, nullIfNotEnoughScores = false) => LazyCalcN(key, n, true, nullIfNotEnoughScores); -type RatingFunctions = { - [G in Game]: { - [P in Playtypes[G]]: ( - game: Game, - playtype: Playtype, - userID: integer, - logger: KtLogger - ) => Promise; - }; -}; +type RatingFunction = ( + game: Game, + playtype: Playtype, + userID: integer, + logger: KtLogger +) => Promise; -const RatingFunctions: RatingFunctions = { - iidx: { - SP: async (g, p, u) => ({ - BPI: await LazyMeanN("BPI", 20, true)(g, p, u), - ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u), - }), - DP: async (g, p, u) => ({ - BPI: await LazyMeanN("BPI", 20, true)(g, p, u), - ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u), - }), - }, - sdvx: { - Single: async (g, p, u) => ({ - VF6: await LazySumN("VF6", 50)(g, p, u), - }), - }, - usc: { - Keyboard: async (g, p, u) => ({ - VF6: await LazySumN("VF6", 50)(g, p, u), - }), - Controller: async (g, p, u) => ({ - VF6: await LazySumN("VF6", 50)(g, p, u), - }), - }, - ddr: { - SP: async (g, p, u) => ({ - MFCP: await LazySumAll("MFCP")(g, p, u), - ktRating: await LazyMeanN("ktRating", 20)(g, p, u), - }), - DP: async (g, p, u) => ({ - MFCP: await LazySumAll("MFCP")(g, p, u), - ktRating: await LazyMeanN("ktRating", 20)(g, p, u), - }), - }, - gitadora: { - Gita: async (g, p, u, l) => ({ - skill: await CalculateGitadoraSkill(g, p, u, l), - naiveSkill: await LazySumN("skill", 50)(g, p, u), - }), - Dora: async (g, p, u, l) => ({ - skill: await CalculateGitadoraSkill(g, p, u, l), - naiveSkill: await LazySumN("skill", 50)(g, p, u), - }), - }, - bms: { - "7K": async (g, p, u) => ({ - sieglinde: await LazyMeanN("sieglinde", 20)(g, p, u), - }), - "14K": async (g, p, u) => ({ - sieglinde: await LazyMeanN("sieglinde", 20)(g, p, u), - }), - }, - pms: { - Controller: async (g, p, u) => ({ - sieglinde: await LazyMeanN("sieglinde", 20)(g, p, u), - }), - Keyboard: async (g, p, u) => ({ - sieglinde: await LazyMeanN("sieglinde", 20)(g, p, u), - }), - }, - chunithm: { - Single: async (g, p, u) => ({ - naiveRating: await LazyMeanN("rating", 20)(g, p, u), - }), - }, - popn: { - "9B": async (g, p, u) => ({ - naiveClassPoints: await LazyMeanN("classPoints", 20)(g, p, u), - }), - }, - museca: { - Single: async (g, p, u) => ({ - ktRating: await LazyMeanN("ktRating", 20)(g, p, u), - }), - }, - maimai: { - Single: async (g, p, u) => ({ - ktRating: await LazyMeanN("ktRating", 20)(g, p, u), - }), - }, - wacca: { - Single: async (g, p, u, l) => ({ - naiveRate: await LazySumN("rate", 50)(g, p, u), - rate: await CalculateWACCARate(g, p, u, l), - }), - }, - jubeat: { - Single: async (g, p, u, l) => ({ - jubility: await CalculateJubility(g, p, u, l), - naiveJubility: await LazySumN("jubility", 60)(g, p, u), - }), - }, -}; +function GetGPTRatingFunction(game: Game, playtype: Playtype): RatingFunction { + switch (`${game}:${playtype}` as IDStrings) { + case "iidx:SP": + case "iidx:DP": + return async (g, p, u) => ({ + BPI: await LazyMeanN("BPI", 20, true)(g, p, u), + ktLampRating: await LazyMeanN("ktLampRating", 20)(g, p, u), + }); + case "sdvx:Single": + case "usc:Keyboard": + case "usc:Controller": + return async (g, p, u) => ({ + VF6: await LazySumN("VF6", 50)(g, p, u), + }); + case "ddr:SP": + case "ddr:DP": + return async (g, p, u) => ({ + MFCP: await LazySumAll("MFCP")(g, p, u), + ktRating: await LazyMeanN("ktRating", 20)(g, p, u), + }); + case "gitadora:Dora": + case "gitadora:Gita": + return async (g, p, u, l) => ({ + skill: await CalculateGitadoraSkill(g, p, u, l), + naiveSkill: await LazySumN("skill", 50)(g, p, u), + }); + case "bms:7K": + case "bms:14K": + case "pms:Keyboard": + case "pms:Controller": + return async (g, p, u) => ({ + sieglinde: await LazyMeanN("sieglinde", 20)(g, p, u), + }); + case "chunithm:Single": + return async (g, p, u) => ({ + naiveRating: await LazyMeanN("rating", 20)(g, p, u), + }); + case "popn:9B": + return async (g, p, u) => ({ + naiveClassPoints: await LazyMeanN("classPoints", 20)(g, p, u), + }); + case "museca:Single": + return async (g, p, u) => ({ + ktRating: await LazyMeanN("ktRating", 20)(g, p, u), + }); + case "maimai:Single": + return async (g, p, u) => ({ + ktRating: await LazyMeanN("ktRating", 20)(g, p, u), + }); + case "wacca:Single": + return async (g, p, u, l) => ({ + naiveRate: await LazySumN("rate", 50)(g, p, u), + rate: await CalculateWACCARate(g, p, u, l), + }); + case "jubeat:Single": + return async (g, p, u, l) => ({ + jubility: await CalculateJubility(g, p, u, l), + naiveJubility: await LazySumN("jubility", 60)(g, p, u), + }); + } +} export function CalculateRatings( game: Game, playtype: Playtype, userID: integer, logger: KtLogger -): Promise>> { - // @ts-expect-error too lazy to type this properly - return RatingFunctions[game][playtype](game, playtype, userID, logger); +): Promise>> { + const RatingFunction = GetGPTRatingFunction(game, playtype); + + return RatingFunction(game, playtype, userID, logger); } // Wacca has a funny algorithm for rate involving gitadora-style latest chart bonuses, @@ -208,7 +172,7 @@ async function CalculateWACCARate( game: Game, playtype: Playtype, userID: integer, - logger: KtLogger + _logger: KtLogger ) { const hotChartIDs = ( await db.charts.wacca.find({ "data.isHot": true }, { projection: { chartID: 1 } }) @@ -270,7 +234,7 @@ async function CalculateGitadoraSkill( game: Game, playtype: Playtype, userID: integer, - logger: KtLogger + _logger: KtLogger ) { const hotSongIDs = ( await db.songs.gitadora.find({ "data.isHot": true }, { projection: { id: 1 } }) @@ -306,7 +270,7 @@ export async function GetBestRatingOnSongs( ratingProp: "skill", limit = 25 ): Promise> { - const r = await db["personal-bests"].aggregate([ + const r: Array<{ doc: PBScoreDocument }> = await db["personal-bests"].aggregate([ { $match: { game, @@ -331,14 +295,14 @@ export async function GetBestRatingOnSongs( }, ]); - return r.map((e: { _id: integer; doc: PBScoreDocument }) => e.doc); + return r.map((e) => e.doc); } async function CalculateJubility( game: Game, playtype: Playtype, userID: integer, - logger: KtLogger + _logger: KtLogger ): Promise { const hotSongs = await db.songs.jubeat.find( { "data.displayVersion": "festo" }, diff --git a/server/src/lib/score-import/framework/user-game-stats/update-ugs.test.ts b/server/src/lib/score-import/framework/user-game-stats/update-ugs.test.ts index 2b188b982..9b9aa3481 100644 --- a/server/src/lib/score-import/framework/user-game-stats/update-ugs.test.ts +++ b/server/src/lib/score-import/framework/user-game-stats/update-ugs.test.ts @@ -1,11 +1,11 @@ -import crypto from "crypto"; +import { UpdateUsersGamePlaytypeStats } from "./update-ugs"; 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 { TestingIIDXSPScorePB } from "test-utils/test-data"; -import { UpdateUsersGamePlaytypeStats } from "./update-ugs"; +import crypto from "crypto"; const logger = CreateLogCtx(__filename); @@ -116,7 +116,7 @@ t.test("#UpdateUsersGamePlaytypeStats", (t) => { "iidx", "SP", 1, - () => ({ dan: 18 }), // lmao + () => ({ dan: 18 }), logger ); @@ -170,7 +170,7 @@ t.test("#UpdateUsersGamePlaytypeStats", (t) => { "iidx", "SP", 1, - () => ({ dan: 18 }), // lmao + () => ({ dan: 18 }), logger ); diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.test.ts b/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.test.ts index bed0daf66..14350654d 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.test.ts @@ -1,9 +1,9 @@ +import { CreateArcIIDXClassHandler } from "./class-handler"; import { IIDXDans } from "lib/constants/classes"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import { MockJSONFetch } from "test-utils/mock-fetch"; import ResetDBState from "test-utils/resets"; -import { CreateArcIIDXClassHandler } from "./class-handler"; const logger = CreateLogCtx(__filename); @@ -36,6 +36,7 @@ t.test("#CreateArcIIDXClassHandler", (t) => { t.equal(res!.dan, IIDXDans.CHUUDEN); const res2 = await fn("iidx", "DP", 1, {}, logger); + t.equal(res2!.dan, IIDXDans.DAN_2); t.end(); @@ -66,6 +67,7 @@ t.test("#CreateArcIIDXClassHandler", (t) => { t.strictSame(res, {}); const res2 = await fn("iidx", "DP", 1, {}, logger); + t.strictSame(res2, {}); t.end(); @@ -87,6 +89,7 @@ t.test("#CreateArcIIDXClassHandler", (t) => { t.strictSame(res, {}); const res2 = await fn("iidx", "DP", 1, {}, logger); + t.strictSame(res2, {}); t.end(); diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.ts b/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.ts index 6bd70ea6f..caf2a8e33 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/class-handler.ts @@ -1,7 +1,7 @@ import { IIDXDans } from "lib/constants/classes"; import { ServerConfig } from "lib/setup/config"; import nodeFetch from "utils/fetch"; -import { HasOwnProperty } from "utils/misc"; +import { HasOwnProperty, IsRecord } from "utils/misc"; import { CreateURLWithParams } from "utils/url"; import type { ClassHandler } from "../../../framework/user-game-stats/types"; @@ -10,8 +10,7 @@ export async function CreateArcIIDXClassHandler( token: string, fetch = nodeFetch ): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let json: any; + let json: unknown; let err: unknown; // SP and DP dans are located in the same place, @@ -27,13 +26,13 @@ export async function CreateArcIIDXClassHandler( }, }); - json = await res.json(); + json = (await res.json()) as unknown; } catch (e) { err = e; } return (game, playtype, userID, ratings, logger) => { - if (err) { + if (err !== undefined) { logger.error( `An error occured while updating classes for ${ServerConfig.ARC_API_URL}.`, { err } @@ -41,15 +40,24 @@ export async function CreateArcIIDXClassHandler( return {}; } + if (!IsRecord(json)) { + logger.warn(`ARC returned a non-record as their JSON? Can't handle class updates.`, { + json, + }); + return {}; + } + // we're just going to lazily path directly towards the rank. // if ARC sends us an unexpected JSON structure or whatever // we wont crash. let arcClass: string | null | undefined; if (playtype === "SP") { - arcClass = json?._items?.[0]?.sp?.rank; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + arcClass = (json as any)._items?.[0]?.sp?.rank; } else if (playtype === "DP") { - arcClass = json?._items?.[0]?.dp?.rank; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + arcClass = (json as any)._items?.[0]?.dp?.rank; } else { logger.error(`ARCIIDXClassUpdater called with invalid playtype of ${playtype}.`); return {}; @@ -84,8 +92,13 @@ const ARCClasses = { 五段: IIDXDans.DAN_5, 四段: IIDXDans.DAN_4, 三段: IIDXDans.DAN_3, - 二段: IIDXDans.DAN_2, // These two look very similar but they aren't - ニ段: IIDXDans.DAN_2, // and ARC uses both, from what I can tell. + + // These two look very similar but they aren't + // and ARC uses both, from what I can tell. + // How nice of them. + 二段: IIDXDans.DAN_2, + ニ段: IIDXDans.DAN_2, + 初段: IIDXDans.DAN_1, 一級: IIDXDans.KYU_1, 二級: IIDXDans.KYU_2, diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/converter.test.ts b/server/src/lib/score-import/import-types/api/arc-iidx/converter.test.ts index 4ab0e89ab..f05f477cd 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/converter.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/converter.test.ts @@ -1,10 +1,10 @@ +import { ConvertAPIArcIIDX, ResolveARCIIDXLamp } from "./converter"; import deepmerge from "deepmerge"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import ResetDBState from "test-utils/resets"; import { GetKTDataJSON, Testing511Song, Testing511SPA } from "test-utils/test-data"; -import { ConvertAPIArcIIDX, ResolveARCIIDXLamp } from "./converter"; -import { ARCIIDXScore } from "./types"; +import type { ARCIIDXScore } from "./types"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/parser.test.ts b/server/src/lib/score-import/import-types/api/arc-iidx/parser.test.ts index 11bf755c6..5e04b5d2c 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/parser.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/parser.test.ts @@ -1,10 +1,10 @@ +import { ParseArcIIDX } from "./parser"; 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 { ParseArcIIDX } from "./parser"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/import-types/api/arc-sdvx/converter.test.ts b/server/src/lib/score-import/import-types/api/arc-sdvx/converter.test.ts index aa34155e8..f3fc83471 100644 --- a/server/src/lib/score-import/import-types/api/arc-sdvx/converter.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-sdvx/converter.test.ts @@ -1,10 +1,10 @@ +import { ConvertAPIArcSDVX, ResolveARCSDVXLamp } from "./converter"; 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 { ConvertAPIArcSDVX, ResolveARCSDVXLamp } from "./converter"; -import { ARCSDVXScore } from "./types"; +import type { ARCSDVXScore } from "./types"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/import-types/api/arc-sdvx/converter.ts b/server/src/lib/score-import/import-types/api/arc-sdvx/converter.ts index 7f77648c8..6daa26c13 100644 --- a/server/src/lib/score-import/import-types/api/arc-sdvx/converter.ts +++ b/server/src/lib/score-import/import-types/api/arc-sdvx/converter.ts @@ -18,7 +18,7 @@ import type { Lamps } from "tachi-common"; import type { EmptyObject } from "utils/types"; // There's a bunch of other useless fields but we don't care -const PR_ArcSDVXScore: PrudenceSchema = { +const PR_ARC_SDVX_SCORE: PrudenceSchema = { chart_id: "string", score: p.isBoundedInteger(0, 10_000_000), lamp: p.isIn("UC", "PUC", "HC", "PLAY", "CLEAR"), @@ -37,7 +37,7 @@ export const ConvertAPIArcSDVX: ConverterFunction = async importType, logger ) => { - const err = p(data, PR_ArcSDVXScore, {}, { throwOnNonObject: false, allowExcessKeys: true }); + const err = p(data, PR_ARC_SDVX_SCORE, {}, { throwOnNonObject: false, allowExcessKeys: true }); if (err) { throw new InvalidScoreFailure(FormatPrError(err, "Invalid ARC Score: ")); diff --git a/server/src/lib/score-import/import-types/api/arc-sdvx/parser.test.ts b/server/src/lib/score-import/import-types/api/arc-sdvx/parser.test.ts index 21ff66e18..387bff9e5 100644 --- a/server/src/lib/score-import/import-types/api/arc-sdvx/parser.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-sdvx/parser.test.ts @@ -1,10 +1,10 @@ +import { ParseArcSDVX } from "./parser"; 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 { ParseArcSDVX } from "./parser"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.test.ts b/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.test.ts index 9f3dca831..b9b2daed8 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.test.ts @@ -1,10 +1,10 @@ +import { CreateKaiIIDXClassHandler } from "./class-handler"; +import { KaiTypeToBaseURL } from "../utils"; 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"; -import { CreateKaiIIDXClassHandler } from "./class-handler"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.ts b/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.ts index 648ee9f14..e5ae16ecc 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/iidx/class-handler.ts @@ -1,6 +1,7 @@ import { KaiTypeToBaseURL } from "../utils"; import { IIDXDans } from "lib/constants/classes"; import nodeFetch from "utils/fetch"; +import { IsRecord } from "utils/misc"; import type { ClassHandler } from "lib/score-import/framework/user-game-stats/types"; export async function CreateKaiIIDXClassHandler( @@ -8,8 +9,7 @@ export async function CreateKaiIIDXClassHandler( token: string, fetch = nodeFetch ): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let json: any; + let json: unknown; let err: unknown; const baseUrl = KaiTypeToBaseURL(kaiType); @@ -23,32 +23,46 @@ export async function CreateKaiIIDXClassHandler( }, }); - json = await res.json(); - } catch (e) { + json = (await res.json()) as unknown; + } catch (e: unknown) { err = e; } return (game, playtype, userID, ratings, logger) => { - if (err) { + if (err !== undefined) { logger.error(`An error occured while updating classes for ${baseUrl}.`, { err }); return {}; } - let iidxDan: number | null; + if (!IsRecord(json)) { + logger.error(`JSON Returned from server was not an object? Not updating anything.`, { + json, + }); + return {}; + } + + let maybeIIDXDan: unknown; if (playtype === "SP") { - iidxDan = json.sp; + maybeIIDXDan = json.sp; } else if (playtype === "DP") { - iidxDan = json.dp; + maybeIIDXDan = json.dp; } else { logger.warn(`KAIIIDXClassUpdater called with invalid playtype of ${playtype}.`); return {}; } - if (iidxDan === null || iidxDan === undefined) { + if ( + maybeIIDXDan === null || + maybeIIDXDan === undefined || + typeof maybeIIDXDan !== "number" + ) { + logger.info(`User has no ${playtype} dan. Not updating anything.`); return {}; } + const iidxDan: number = maybeIIDXDan; + if (!Number.isInteger(iidxDan)) { logger.warn(`${baseUrl} returned a dan of ${iidxDan}, which was not a number.`); return {}; diff --git a/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.test.ts b/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.test.ts index 63b684c8c..fa7273225 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.test.ts @@ -1,10 +1,10 @@ +import { ConvertAPIKaiIIDX } 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 { LoadTachiIIDXData, Testing511Song, Testing511SPA } from "test-utils/test-data"; -import { ConvertAPIKaiIIDX } from "./converter"; const logger = CreateLogCtx(__filename); @@ -43,6 +43,7 @@ t.test("#ConvertAPIKaiIIDX", (t) => { service: "FLO", scoreData: { grade: "MAX-", + // percent: 99.87277353689568 floating point, score: 1570, lamp: "HARD CLEAR", diff --git a/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.ts b/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.ts index e37801232..3b1b26494 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/iidx/converter.ts @@ -16,7 +16,7 @@ import type { ConverterFunction } from "../../types"; import type { KaiContext, KaiIIDXScore } from "../types"; import type { integer, Lamps } from "tachi-common"; -const PR_KaiIIDXScore = { +const PR_KAI_IIDX_SCORE = { music_id: p.isPositiveInteger, play_style: p.isIn("SINGLE", "DOUBLE"), difficulty: p.isIn("BEGINNER", "NORMAL", "HYPER", "ANOTHER", "LEGGENDARIA"), @@ -199,7 +199,7 @@ export const ConvertAPIKaiIIDX: ConverterFunction = async ( importType, logger ) => { - const err = p(data, PR_KaiIIDXScore, {}, { allowExcessKeys: true }); + const err = p(data, PR_KAI_IIDX_SCORE, {}, { allowExcessKeys: true }); if (err) { throw new InvalidScoreFailure(FormatPrError(err)); @@ -218,7 +218,7 @@ export const ConvertAPIKaiIIDX: ConverterFunction = async ( let musicID = score.music_id; - if (OldLeggendariaConversionTable[score.music_id]) { + if (OldLeggendariaConversionTable[score.music_id] !== undefined) { musicID = OldLeggendariaConversionTable[score.music_id]!; // This is now definitely a leggendaria. diff --git a/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.test.ts b/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.test.ts index d6c7a59fe..c1c3563d0 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.test.ts @@ -1,8 +1,8 @@ +import { ParseKaiIIDX } from "./parser"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import { MockJSONFetch } from "test-utils/mock-fetch"; import ResetDBState from "test-utils/resets"; -import { ParseKaiIIDX } from "./parser"; const fakeAuth = { userID: 1, diff --git a/server/src/lib/score-import/import-types/common/api-kai/reauth.test.ts b/server/src/lib/score-import/import-types/common/api-kai/reauth.test.ts index 8696e180e..4160f5184 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/reauth.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/reauth.test.ts @@ -1,12 +1,12 @@ +import { CreateKaiReauthFunction } from "./reauth"; import deepmerge from "deepmerge"; import db from "external/mongo/db"; 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"; +import type { KaiAuthDocument } from "tachi-common"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/import-types/common/api-kai/reauth.ts b/server/src/lib/score-import/import-types/common/api-kai/reauth.ts index dfc268806..3bbe6c74b 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/reauth.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/reauth.ts @@ -66,7 +66,7 @@ export function CreateKaiReauthFunction( /* istanbul ignore next */ try { - json = await res.json(); + json = (await res.json()) as unknown; } catch (err) { logger.error(`Invalid JSON body in successful reauth response.`, { res, err }); throw new ScoreImportFatalError( @@ -85,6 +85,12 @@ export function CreateKaiReauthFunction( ); } + // asserted by prudence + const validatedContent = json as { + refresh_token: string; + access_token: string; + }; + await db["kai-auth-tokens"].update( { userID: authDoc.userID, @@ -92,12 +98,12 @@ export function CreateKaiReauthFunction( }, { $set: { - refreshToken: json.refresh_token, - token: json.access_token, + refreshToken: validatedContent.refresh_token, + token: validatedContent.access_token, }, } ); - return json.access_token; + return validatedContent.access_token; }; } diff --git a/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.test.ts b/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.test.ts index 737cd38bc..7cbf90a67 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.test.ts @@ -1,10 +1,10 @@ +import { CreateKaiSDVXClassHandler } from "./class-handler"; +import { KaiTypeToBaseURL } from "../utils"; import { SDVXDans } 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"; -import { CreateKaiSDVXClassHandler } from "./class-handler"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.ts b/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.ts index c9e7cf325..4d6b339b4 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/sdvx/class-handler.ts @@ -1,6 +1,7 @@ import { KaiTypeToBaseURL } from "../utils"; import { SDVXDans } from "lib/constants/classes"; import nodeFetch from "utils/fetch"; +import { IsRecord } from "utils/misc"; import type { ClassHandler } from "lib/score-import/framework/user-game-stats/types"; export async function CreateKaiSDVXClassHandler( @@ -8,8 +9,7 @@ export async function CreateKaiSDVXClassHandler( token: string, fetch = nodeFetch ): Promise { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let json: any; + let json: unknown; let err: unknown; const baseUrl = KaiTypeToBaseURL(kaiType); @@ -23,18 +23,29 @@ export async function CreateKaiSDVXClassHandler( }, }); - json = await res.json(); - } catch (e) { + json = (await res.json()) as unknown; + } catch (e: unknown) { err = e; } return (game, playtype, userID, ratings, logger) => { - if (err) { + if (err !== undefined) { logger.error(`An error occured while updating classes for ${baseUrl}.`, { err }); return {}; } - if (json.skill_level === null || json.skill_level === undefined) { + if (!IsRecord(json)) { + logger.error(`JSON Returned from server was not an object? Not updating anything.`, { + json, + }); + return {}; + } + + if ( + json.skill_level === null || + json.skill_level === undefined || + typeof json.skill_level !== "number" + ) { logger.info(`User has no skill_level. Not updating anything.`); return {}; } diff --git a/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.test.ts b/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.test.ts index fe0c329bc..e2bf0355f 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.test.ts @@ -1,10 +1,10 @@ +import { ConvertAPIKaiSDVX, ConvertDifficulty, ConvertVersion, ResolveKaiLamp } 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 { TestingAlbidaADV, TestingSDVXAlbidaSong } from "test-utils/test-data"; -import { ConvertAPIKaiSDVX, ConvertDifficulty, ConvertVersion, ResolveKaiLamp } from "./converter"; const logger = CreateLogCtx(__filename); @@ -12,7 +12,10 @@ const sdvxScore = { sdvx_id: 32157055, music_id: 1, music_difficulty: 1, - played_version: 6, // exg + + // exg + played_version: 6, + clear_type: 2, max_chain: 179, score: 9310699, @@ -44,6 +47,7 @@ t.test("#ConvertAPIKaiSDVX", (t) => { service: "FLO", scoreData: { grade: "AA", + // percent: 93.10699, floating point score: 9310699, lamp: "CLEAR", diff --git a/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.ts b/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.ts index c3050eab2..5def625e5 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/sdvx/converter.ts @@ -16,7 +16,7 @@ import type { ConverterFunction } from "../../types"; import type { KaiContext, KaiSDVXScore } from "../types"; import type { Lamps } from "tachi-common"; -const PR_KaiSDVXScore = { +const PR_KAI_SDVX_SCORE = { music_id: p.isPositiveInteger, music_difficulty: p.isBoundedInteger(0, 4), played_version: p.isBoundedInteger(1, 6), @@ -38,7 +38,7 @@ export const ConvertAPIKaiSDVX: ConverterFunction = async ( importType, logger ) => { - const err = p(data, PR_KaiSDVXScore, {}, { allowExcessKeys: true }); + const err = p(data, PR_KAI_SDVX_SCORE, {}, { allowExcessKeys: true }); if (err) { throw new InvalidScoreFailure(FormatPrError(err)); diff --git a/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.test.ts b/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.test.ts index 979f3e1c5..ee4f069e9 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.test.ts @@ -1,8 +1,8 @@ +import { ParseKaiSDVX } from "./parser"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import { MockJSONFetch } from "test-utils/mock-fetch"; import ResetDBState from "test-utils/resets"; -import { ParseKaiSDVX } from "./parser"; const fakeAuth = { userID: 1, diff --git a/server/src/lib/score-import/import-types/common/api-kai/traverse-api.test.ts b/server/src/lib/score-import/import-types/common/api-kai/traverse-api.test.ts index 3ff89c8b1..3593ad6b3 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/traverse-api.test.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/traverse-api.test.ts @@ -1,11 +1,11 @@ +import { TraverseKaiAPI } from "./traverse-api"; +import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import { agta } from "test-utils/misc"; import { MockBasicFetch, MockJSONFetch } from "test-utils/mock-fetch"; import ResetDBState from "test-utils/resets"; -import { NodeFetch } from "utils/fetch"; -import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; -import { TraverseKaiAPI } from "./traverse-api"; +import type { NodeFetch } from "utils/fetch"; const logger = CreateLogCtx(__filename); @@ -77,6 +77,7 @@ t.test("#TraverseKaiAPI", (t) => { const mockKaiAPI = () => { throw new Error("Fake Request timeout..."); }; + const res = TraverseKaiAPI("http://url.com", "/sub", fakeAuth, logger, null, mockKaiAPI); t.rejects(() => agta(res)); @@ -178,7 +179,7 @@ t.test("#TraverseKaiAPI", (t) => { "/sub", fakeAuth, logger, - // eslint-disable-next-line require-await + // eslint-disable-next-line @typescript-eslint/require-await async () => { hasAttemptedReauth = true; return "bar"; @@ -187,6 +188,7 @@ t.test("#TraverseKaiAPI", (t) => { ); await t.rejects( + // eslint-disable-next-line lines-around-comment // not redundant here // eslint-disable-next-line no-return-await async () => await agta(res), diff --git a/server/src/lib/score-import/import-types/common/api-kai/traverse-api.ts b/server/src/lib/score-import/import-types/common/api-kai/traverse-api.ts index 6b7ac732c..a524c6648 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/traverse-api.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/traverse-api.ts @@ -2,6 +2,7 @@ import ScoreImportFatalError from "../../../framework/score-importing/score-impo import { VERSION_STR } from "lib/constants/version"; import { TachiConfig } from "lib/setup/config"; import nodeFetch from "utils/fetch"; +import { IsRecord } from "utils/misc"; import { URL } from "url"; import type { KtLogger } from "lib/logger/logger"; @@ -113,19 +114,27 @@ export async function* TraverseKaiAPI( try { // eslint-disable-next-line no-await-in-loop - json = await res.json(); + json = (await res.json()) as unknown; } catch (err) { logger.error( - `received invalid (non-json) response from ${url}. Status code was ${res.status}.`, + `Received invalid (non-json) response from ${url}. Status code was ${res.status}.`, { err } ); throw new ScoreImportFatalError( 500, - `received invalid response from their API. Are they down?` + `Received invalid response from their API. Are they down?` ); } - if (json._links === null || typeof json._links !== "object") { + if (!IsRecord(json)) { + logger.error(`Recieved invalid (non-object, but json) response from ${url}.`, { json }); + throw new ScoreImportFatalError( + 500, + `Received invalid response from their API. Are they down?` + ); + } + + if (!IsRecord(json._links)) { logger.error(`Received invalid JSON from ${url}. Invalid _links.`, { body: json }); throw new ScoreImportFatalError( @@ -152,22 +161,22 @@ export async function* TraverseKaiAPI( // exit the loop after this, we're on the last page. fetchMoreData = false; } else { - logger.error(`received invalid response from ${url}. Invalid _links._next.`, { + logger.error(`Received invalid response from ${url}. Invalid _links._next.`, { body: json, }); throw new ScoreImportFatalError( 500, - `received invalid _links._next prop from their API.` + `Received invalid _links._next prop from their API.` ); } if (!Array.isArray(json._items)) { - logger.error(`received invalid response from ${url}. Invalid _items.`, { + logger.error(`Received invalid response from ${url}. Invalid _items.`, { body: json, }); - throw new ScoreImportFatalError(500, `received invalid _items from their API.`); + throw new ScoreImportFatalError(500, `Received invalid _items from their API.`); } // yield everything out of the score array diff --git a/server/src/lib/score-import/import-types/common/api-kai/types.ts b/server/src/lib/score-import/import-types/common/api-kai/types.ts index cecbd4be9..f548ada41 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/types.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/types.ts @@ -7,7 +7,10 @@ export interface KaiIIDXScore { version_played: integer; lamp: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; ex_score: integer; - miss_count: integer | null; // -1 => null + + // -1 => null + miss_count: integer | null; + fast_count: integer | null; slow_count: integer | null; timestamp: string; @@ -17,7 +20,9 @@ export interface KaiSDVXScore { music_id: integer; music_difficulty: 0 | 1 | 2 | 3 | 4; played_version: integer; - clear_type: 0 | 1 | 2 | 3 | 4; // hm + + // 0 here is 'PLAYED' + clear_type: 0 | 1 | 2 | 3 | 4; score: integer; max_chain: integer; critical: integer; diff --git a/server/src/lib/score-import/import-types/common/api-kai/utils.ts b/server/src/lib/score-import/import-types/common/api-kai/utils.ts index 302095f21..03cb20880 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/utils.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/utils.ts @@ -1,27 +1,34 @@ import { ServerConfig } from "lib/setup/config"; export function KaiTypeToBaseURL(kaiType: "EAG" | "FLO" | "MIN") { - if (kaiType === "FLO") { - if (!ServerConfig.FLO_API_URL) { - throw new Error(`Got kaiType FLO, but no API_URL was defined?`); + switch (kaiType) { + case "FLO": { + if (!ServerConfig.FLO_API_URL) { + throw new Error(`Got kaiType FLO, but no API_URL was defined?`); + } + + return ServerConfig.FLO_API_URL; } - return ServerConfig.FLO_API_URL; - } else if (kaiType === "EAG") { - if (!ServerConfig.EAG_API_URL) { - throw new Error(`Got kaiType EAG, but no API_URL was defined?`); + case "EAG": { + if (!ServerConfig.EAG_API_URL) { + throw new Error(`Got kaiType EAG, but no API_URL was defined?`); + } + + return ServerConfig.EAG_API_URL; } - return ServerConfig.EAG_API_URL; - } else if (kaiType === "MIN") { - if (!ServerConfig.MIN_API_URL) { - throw new Error(`Got kaiType MIN, but no API_URL was defined?`); + case "MIN": { + if (!ServerConfig.MIN_API_URL) { + throw new Error(`Got kaiType MIN, but no API_URL was defined?`); + } + + return ServerConfig.MIN_API_URL; } - return ServerConfig.MIN_API_URL; + default: + throw new Error(`Invalid Kai Type ${kaiType} provided.`); } - - throw new Error(`Invalid Kai Type ${kaiType} provided.`); } export function GetKaiTypeClientCredentials(kaiType: "EAG" | "FLO" | "MIN") { diff --git a/server/src/lib/score-import/import-types/common/batch-manual/converter.test.ts b/server/src/lib/score-import/import-types/common/batch-manual/converter.test.ts index eb4485ab1..36e742008 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/converter.test.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/converter.test.ts @@ -1,13 +1,13 @@ +import { ConverterBatchManual, ResolveChartFromSong, ResolveMatchTypeToKTData } from "./converter"; +import { InvalidScoreFailure } from "../../../framework/common/converter-failures"; import deepmerge from "deepmerge"; import CreateLogCtx from "lib/logger/logger"; -import { BatchManualScore, ChartDocument, Game, SongDocument } from "tachi-common"; import t from "tap"; import ResetDBState from "test-utils/resets"; -import { BMSGazerChart, BMSGazerSong, GetKTDataJSON, Testing511Song, Testing511SPA } from "test-utils/test-data"; +import { BMSGazerChart, BMSGazerSong, Testing511Song, Testing511SPA } from "test-utils/test-data"; import { EscapeStringRegexp } from "utils/misc"; -import { InvalidScoreFailure } from "../../../framework/common/converter-failures"; -import { ConverterBatchManual, ResolveChartFromSong, ResolveMatchTypeToKTData } from "./converter"; -import { BatchManualContext } from "./types"; +import type { BatchManualContext } from "./types"; +import type { BatchManualScore, Game } from "tachi-common"; const baseBatchManualScore = { score: 500, @@ -30,7 +30,9 @@ const ktdWrap = (msg: string, game: Game = "iidx", version = null): any => ({ importType: "file/batch-manual", message: new RegExp(EscapeStringRegexp(msg), "u"), converterContext: { game, service: "foo", version }, - data: {}, // any under t.match rules. + + // any under t.match rules. + data: {}, }); const logger = CreateLogCtx(__filename); @@ -57,6 +59,7 @@ t.test("#ResolveMatchTypeToKTData", (t) => { t.rejects( () => ResolveMatchTypeToKTData( + // eslint-disable-next-line lines-around-comment // @ts-expect-error bad deepmerge(baseBatchManualScore, { identifier: "90000" }), context, @@ -349,7 +352,10 @@ t.test("#ResolveChartFromSong", (t) => { t.test("Should return the chart for the song + ptdf", async (t) => { const res = await ResolveChartFromSong( Testing511Song, - baseBatchManualScore, // has playtype + diff + + // has playtype + diff + baseBatchManualScore, + { game: "iidx", service: "foo", playtype: "SP", version: null }, importType ); @@ -398,7 +404,9 @@ t.test("#ResolveChartFromSong", (t) => { () => ResolveChartFromSong( Testing511Song, - deepmerge(baseBatchManualScore, { difficulty: "LEGGENDARIA" }), // 511 has no legg (yet, lol) + + // 511 has no legg (yet, lol) + deepmerge(baseBatchManualScore, { difficulty: "LEGGENDARIA" }), { game: "iidx", service: "foo", version: null, playtype: "SP" }, importType ), @@ -451,6 +459,7 @@ t.test("#ConverterFn", (t) => { lamp: "HARD CLEAR", score: 500, grade: "E", + // percent: 31.5, -- ish, FPA is hard. judgements: {}, hitMeta: {}, @@ -646,6 +655,7 @@ t.test("#ConverterFn", (t) => { lamp: "HARD CLEAR", score: 500, grade: "E", + // percent: 31.5, -- ish, FPA is hard. judgements: {}, hitMeta: {}, @@ -661,6 +671,7 @@ t.test("#ConverterFn", (t) => { t.rejects( () => ConverterBatchManual( + // eslint-disable-next-line lines-around-comment // @ts-expect-error broken deepmerge deepmerge(baseBatchManualScore, { score: 2000 }), { game: "iidx", service: "foo", playtype: "SP", version: null }, diff --git a/server/src/lib/score-import/import-types/common/batch-manual/converter.ts b/server/src/lib/score-import/import-types/common/batch-manual/converter.ts index 765de41db..ccb15dc2a 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/converter.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/converter.ts @@ -53,7 +53,7 @@ export const ConverterBatchManual: ConverterFunction { const game = context.game; - if (data.matchType === "bmsChartHash") { - if (game !== "bms") { - throw new InvalidScoreFailure(`Cannot use bmsChartHash lookup on ${game}.`); + switch (data.matchType) { + case "bmsChartHash": { + if (game !== "bms") { + throw new InvalidScoreFailure(`Cannot use bmsChartHash lookup on ${game}.`); + } + + const chart = await FindBMSChartOnHash(data.identifier); + + if (!chart) { + throw new KTDataNotFoundFailure( + `Cannot find chart for hash ${data.identifier}.`, + importType, + data, + context + ); + } + + if (chart.playtype !== context.playtype) { + throw new InvalidScoreFailure( + `Chart ${chart.chartID}'s playtype was ${chart.playtype}, but this was not equal to the import playtype of ${context.playtype}.` + ); + } + + const song = await FindSongOnID(game, chart.songID); + + if (!song) { + logger.severe(`BMS songID ${chart.songID} has charts but no parent song.`); + throw new InternalFailure( + `BMS songID ${chart.songID} has charts but no parent song.` + ); + } + + return { chart, song }; } - const chart = await FindBMSChartOnHash(data.identifier); + case "ddrSongHash": { + if (game !== "ddr") { + throw new InvalidScoreFailure(`Cannot use ddrSongHash lookup on ${game}.`); + } - if (!chart) { - throw new KTDataNotFoundFailure( - `Cannot find chart for hash ${data.identifier}.`, - importType, - data, - context + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (!data.difficulty) { + throw new InvalidScoreFailure( + `Missing 'difficulty' field, but is needed for ddrSongHash lookup.` + ); + } + + const difficulty = AssertStrAsDifficulty(data.difficulty, game, context.playtype); + + const chart = await FindDDRChartOnSongHash( + data.identifier, + context.playtype, + difficulty ); + + if (!chart) { + throw new KTDataNotFoundFailure( + `Cannot find chart for songHash ${data.identifier} (${context.playtype} ${difficulty}).`, + importType, + data, + context + ); + } + + const song = await FindSongOnID(game, chart.songID); + + if (!song) { + logger.severe(`DDR songID ${chart.songID} has charts but no parent song.`); + throw new InternalFailure( + `DDR songID ${chart.songID} has charts but no parent song.` + ); + } + + return { song, chart }; } - if (chart.playtype !== context.playtype) { - throw new InvalidScoreFailure( - `Chart ${chart.chartID}'s playtype was ${chart.playtype}, but this was not equal to the import playtype of ${context.playtype}.` + case "popnChartHash": { + if (game !== "popn") { + throw new InvalidScoreFailure(`Cannot use popnChartHash lookup on ${game}.`); + } + + const chart = await db.charts.popn.findOne({ + playtype: context.playtype, + "data.hashSHA256": data.identifier, + }); + + if (!chart) { + throw new KTDataNotFoundFailure( + `Cannot find chart for popnChartHash ${data.identifier} (${context.playtype}).`, + importType, + data, + context + ); + } + + const song = await FindSongOnID(game, chart.songID); + + if (!song) { + logger.severe(`Pop'n songID ${chart.songID} has charts but no parent song.`); + throw new InternalFailure( + `Pop'n songID ${chart.songID} has charts but no parent song.` + ); + } + + return { song, chart }; + } + + case "tachiSongID": { + const songID = AssertStrAsPositiveInt( + data.identifier, + "Invalid songID - must be a stringified positive integer." ); + + const song = await FindSongOnID(game, songID); + + if (!song) { + throw new KTDataNotFoundFailure( + `Cannot find song with songID ${data.identifier}.`, + importType, + data, + context + ); + } + + const chart = await ResolveChartFromSong(song, data, context, importType); + + return { song, chart }; } - const song = await FindSongOnID(game, chart.songID); + case "songTitle": { + const song = await FindSongOnTitleInsensitive(game, data.identifier); - if (!song) { - logger.severe(`BMS songID ${chart.songID} has charts but no parent song.`); - throw new InternalFailure(`BMS songID ${chart.songID} has charts but no parent song.`); + if (!song) { + throw new KTDataNotFoundFailure( + `Cannot find song with title ${data.identifier}.`, + importType, + data, + context + ); + } + + const chart = await ResolveChartFromSong(song, data, context, importType); + + return { song, chart }; } - return { chart, song }; - } else if (data.matchType === "ddrSongHash") { - if (game !== "ddr") { - throw new InvalidScoreFailure(`Cannot use ddrSongHash lookup on ${game}.`); + case "inGameID": { + const gamesWithInGameIDSupport = [ + "iidx", + "popn", + "ddr", + "sdvx", + "jubeat", + "chunithm", + "gitadora", + "maimai", + "museca", + ]; + + if (!gamesWithInGameIDSupport.includes(game)) { + throw new InvalidScoreFailure( + `Cannot use inGameID on game ${game}. The game may not have a concept of an in game ID, or support just might not exist yet.` + ); + } + + let identifier: number | string = data.identifier; + + // ddr uses weird strings as IDs instead of numbers + + if (game !== "ddr") { + identifier = Number(data.identifier); + } + + const difficulty = AssertStrAsDifficulty(data.difficulty, game, context.playtype); + + const chart = await db.charts[game].findOne({ + "data.inGameID": identifier, + playtype: context.playtype, + difficulty, + }); + + if (!chart) { + throw new KTDataNotFoundFailure( + `Cannot find chart for inGameID ${data.identifier} (${context.playtype}).`, + importType, + data, + context + ); + } + + const song = await db.songs[game].findOne({ id: chart.songID }); + + if (!song) { + logger.severe(`Song-Chart desync on ${chart.songID}.`); + throw new InternalFailure(`Failed to get song for a chart that exists.`); + } + + return { song, chart }; } - if (!data.difficulty) { - throw new InvalidScoreFailure( - `Missing 'difficulty' field, but is needed for ddrSongHash lookup.` + case "uscChartHash": { + if (game !== "usc") { + throw new InvalidScoreFailure(`uscChartMash matchType can only be used on USC.`); + } + + const chart = await db.charts.usc.findOne({ + "data.hashSHA1": data.identifier, + playtype: context.playtype, + }); + + if (!chart) { + throw new KTDataNotFoundFailure( + `Cannot find chart with hash ${data.identifier}.`, + importType, + data, + context + ); + } + + const song = await db.songs[game].findOne({ id: chart.songID }); + + if (!song) { + logger.severe(`Song-Chart desync on ${chart.songID}.`); + throw new InternalFailure(`Failed to get song for a chart that exists.`); + } + + return { song, chart }; + } + + default: { + const { matchType } = data; + + logger.error( + `Invalid matchType ${matchType} ended up in conversion - should have been rejected by prudence?` ); + + // really, this could be a larger error. - it's an internal failure because prudence should reject this. + throw new InvalidScoreFailure(`Invalid matchType ${matchType}.`); } - - const difficulty = AssertStrAsDifficulty(data.difficulty, game, context.playtype); - - const chart = await FindDDRChartOnSongHash(data.identifier, context.playtype, difficulty); - - if (!chart) { - throw new KTDataNotFoundFailure( - `Cannot find chart for songHash ${data.identifier} (${context.playtype} ${difficulty}).`, - importType, - data, - context - ); - } - - const song = await FindSongOnID(game, chart.songID); - - if (!song) { - logger.severe(`DDR songID ${chart.songID} has charts but no parent song.`); - throw new InternalFailure(`DDR songID ${chart.songID} has charts but no parent song.`); - } - - return { song, chart }; - } else if (data.matchType === "popnChartHash") { - if (game !== "popn") { - throw new InvalidScoreFailure(`Cannot use popnChartHash lookup on ${game}.`); - } - - const chart = await db.charts.popn.findOne({ - playtype: context.playtype, - "data.hashSHA256": data.identifier, - }); - - if (!chart) { - throw new KTDataNotFoundFailure( - `Cannot find chart for popnChartHash ${data.identifier} (${context.playtype}).`, - importType, - data, - context - ); - } - - const song = await FindSongOnID(game, chart.songID); - - if (!song) { - logger.severe(`Pop'n songID ${chart.songID} has charts but no parent song.`); - throw new InternalFailure( - `Pop'n songID ${chart.songID} has charts but no parent song.` - ); - } - - return { song, chart }; - } else if (data.matchType === "tachiSongID") { - const songID = AssertStrAsPositiveInt( - data.identifier, - "Invalid songID - must be a stringified positive integer." - ); - - const song = await FindSongOnID(game, songID); - - if (!song) { - throw new KTDataNotFoundFailure( - `Cannot find song with songID ${data.identifier}.`, - importType, - data, - context - ); - } - - const chart = await ResolveChartFromSong(song, data, context, importType); - - return { song, chart }; - } else if (data.matchType === "songTitle") { - const song = await FindSongOnTitleInsensitive(game, data.identifier); - - if (!song) { - throw new KTDataNotFoundFailure( - `Cannot find song with title ${data.identifier}.`, - importType, - data, - context - ); - } - - const chart = await ResolveChartFromSong(song, data, context, importType); - - return { song, chart }; - } else if (data.matchType === "inGameID") { - const gamesWithInGameIDSupport = [ - "iidx", - "popn", - "ddr", - "sdvx", - "jubeat", - "chunithm", - "gitadora", - "maimai", - "museca", - ]; - - if (!gamesWithInGameIDSupport.includes(game)) { - throw new InvalidScoreFailure( - `Cannot use inGameID on game ${game}. The game may not have a concept of an in game ID, or support just might not exist yet.` - ); - } - - let identifier: number | string = data.identifier; - - // ddr uses weird strings as IDs instead of numbers - - if (game !== "ddr") { - identifier = Number(data.identifier); - } - - const difficulty = AssertStrAsDifficulty(data.difficulty, game, context.playtype); - - const chart = await db.charts[game].findOne({ - "data.inGameID": identifier, - playtype: context.playtype, - difficulty, - }); - - if (!chart) { - throw new KTDataNotFoundFailure( - `Cannot find chart for inGameID ${data.identifier} (${context.playtype}).`, - importType, - data, - context - ); - } - - const song = await db.songs[game].findOne({ id: chart.songID }); - - if (!song) { - logger.severe(`Song-Chart desync on ${chart.songID}.`); - throw new InternalFailure(`Failed to get song for a chart that exists.`); - } - - return { song, chart }; - } else if (data.matchType === "uscChartHash") { - if (game !== "usc") { - throw new InvalidScoreFailure(`uscChartMash matchType can only be used on USC.`); - } - - const chart = await db.charts.usc.findOne({ - "data.hashSHA1": data.identifier, - playtype: context.playtype, - }); - - if (!chart) { - throw new KTDataNotFoundFailure( - `Cannot find chart with hash ${data.identifier}.`, - importType, - data, - context - ); - } - - const song = await db.songs[game].findOne({ id: chart.songID }); - - if (!song) { - logger.severe(`Song-Chart desync on ${chart.songID}.`); - throw new InternalFailure(`Failed to get song for a chart that exists.`); - } - - return { song, chart }; } - - logger.error( - `Invalid matchType ${data.matchType} ended up in conversion - should have been rejected by prudence?` - ); - - // really, this could be a larger error. - it's an internal failure because prudence should reject this. - throw new InvalidScoreFailure(`Invalid matchType ${data.matchType}.`); } export async function ResolveChartFromSong( diff --git a/server/src/lib/score-import/import-types/common/batch-manual/parser.test.ts b/server/src/lib/score-import/import-types/common/batch-manual/parser.test.ts index 2b13635c6..d363b7521 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/parser.test.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/parser.test.ts @@ -1,13 +1,13 @@ +import { ParseBatchManualFromObject as ParserFn } from "./parser"; +import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; import deepmerge from "deepmerge"; import { IIDXDans } from "lib/constants/classes"; import CreateLogCtx from "lib/logger/logger"; -import { BatchManual } from "tachi-common"; import t from "tap"; import { EscapeStringRegexp } from "utils/misc"; -import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; -import { ParseBatchManualFromObject as ParserFn } from "./parser"; +import type { BatchManual } from "tachi-common"; -const mockErr = (...msg: string[]) => +const mockErr = (...msg: Array) => ({ statusCode: 400, message: new RegExp(msg.map((e) => `${EscapeStringRegexp(e)}.*`).join(""), "u"), @@ -370,7 +370,9 @@ t.test("#ParserFn", (t) => { scores: [ { score: 1000, - lamp: "ALL JUSTICE", // not an iidx lamp + + // not an iidx lamp + lamp: "ALL JUSTICE", matchType: "tachiSongID", identifier: "123", difficulty: "ANOTHER", diff --git a/server/src/lib/score-import/import-types/common/batch-manual/parser.ts b/server/src/lib/score-import/import-types/common/batch-manual/parser.ts index 846b3c08f..5c92cfd13 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/parser.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/parser.ts @@ -4,6 +4,7 @@ import deepmerge from "deepmerge"; import { TachiConfig } from "lib/setup/config"; import p from "prudence"; import { GetGameConfig, GetGamePTConfig } from "tachi-common"; +import { IsRecord, IsValidGame, IsValidPlaytype } from "utils/misc"; import { FormatPrError } from "utils/prudence"; import type { ParserFunctionReturns } from "../types"; import type { BatchManualContext } from "./types"; @@ -19,7 +20,7 @@ const BaseValidHitMeta = { maxCombo: optNull(p.isPositiveInteger), }; -const PR_DPRandom = (self: unknown) => { +const PR_DP_RANDOM = (self: unknown) => { if (!Array.isArray(self) || self.length !== 2) { return "Expected an array with length 2"; } @@ -33,12 +34,12 @@ const PR_DPRandom = (self: unknown) => { return true; }; -const PR_ScoreMeta = (game: Game, playtype: Playtype): PrudenceSchema => { +const PR_SCORE_META = (game: Game, playtype: Playtype): PrudenceSchema => { if (game === "iidx") { const random = playtype === "SP" ? optNull(p.isIn("NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR")) - : PR_DPRandom; + : PR_DP_RANDOM; return { random, @@ -50,7 +51,7 @@ const PR_ScoreMeta = (game: Game, playtype: Playtype): PrudenceSchema => { const random = playtype === "7K" ? optNull(p.isIn("NONRAN", "RANDOM", "R-RANDOM", "S-RANDOM", "MIRROR")) - : optNull(PR_DPRandom); + : optNull(PR_DP_RANDOM); return { random, @@ -81,7 +82,7 @@ const PR_ScoreMeta = (game: Game, playtype: Playtype): PrudenceSchema => { return {}; }; -const PR_HitMeta = (game: Game): PrudenceSchema => { +const PR_HIT_META = (game: Game): PrudenceSchema => { if (game === "iidx") { return { bp: optNull(p.isPositiveInteger), @@ -142,7 +143,7 @@ const PR_HitMeta = (game: Game): PrudenceSchema => { return {}; }; -const PR_BatchManualScore = (game: Game, playtype: Playtype): PrudenceSchema => { +const PR_BATCH_MANUAL_SCORE = (game: Game, playtype: Playtype): PrudenceSchema => { const gptConfig = GetGamePTConfig(game, playtype); return { @@ -160,9 +161,11 @@ const PR_BatchManualScore = (game: Game, playtype: Playtype): PrudenceSchema => identifier: "string", percent: game === "jubeat" ? p.isBetween(0, 120) : p.is(undefined), comment: optNull(p.isBoundedString(3, 240)), - difficulty: "*?string", // this is checked in converting instead - // september 9th 2001 - this saves people who dont - // read any documentation. + difficulty: "*?string", + + // this is checked in converting instead + // the lowest acceptable time is september 9th 2001 - this check saves people who dont + // read any documentation and would otherwise submit garbage. timeAchieved: optNull( (self) => (typeof self === "number" && self > 1_000_000_000_000) || @@ -174,26 +177,24 @@ const PR_BatchManualScore = (game: Game, playtype: Playtype): PrudenceSchema => return "Not a valid object."; } - for (const key in self) { - // @ts-expect-error check + for (const [key, v] of Object.entries(self)) { + // @ts-expect-error Frustrating check as it's a string but not a judgement so it's not really + // happy with it. if (!gptConfig.judgements.includes(key)) { return `Invalid Key ${key}. Expected any of ${gptConfig.judgements.join(", ")}`; } - // @ts-expect-error shush - const v = self[key]; - if ((!Number.isSafeInteger(v) || v < 0) && v !== null) { - return `Key ${key} had an invalid value of ${v} [${typeof v}]`; + return `Key ${key} had an invalid value of ${v} [type: ${typeof v}]`; } } return true; }), hitMeta: optNull( - deepmerge(BaseValidHitMeta, PR_HitMeta(game)) as unknown as ValidSchemaValue + deepmerge(BaseValidHitMeta, PR_HIT_META(game)) as unknown as ValidSchemaValue ), - scoreMeta: optNull(PR_ScoreMeta(game, playtype)), + scoreMeta: optNull(PR_SCORE_META(game, playtype)), }; }; @@ -204,9 +205,9 @@ const WaccaStringStageUps = GetGamePTConfig("wacca", "Single").classHumanisedFor (e) => e.id ); -const PR_BatchManualClasses = (game: Game): PrudenceSchema => { +const PR_BATCH_MANUAL_CLASSES = (game: Game): PrudenceSchema => { + // This can be implemented for any non-static class (i.e. dans). switch (game) { - // This can be implemented for any non-static class (i.e. dans). case "iidx": return { dan: optNull(p.isIn(IIDXStringDans)), @@ -224,15 +225,15 @@ const PR_BatchManualClasses = (game: Game): PrudenceSchema => { } }; -const PR_BatchManual = (game: Game, playtype: Playtype): PrudenceSchema => ({ +const PR_BATCH_MANUAL = (game: Game, playtype: Playtype): PrudenceSchema => ({ meta: { service: p.isBoundedString(3, 15), game: p.isIn(TachiConfig.GAMES), playtype: p.is(playtype), version: "*?string", }, - scores: [PR_BatchManualScore(game, playtype)], - classes: optNull(PR_BatchManualClasses(game)), + scores: [PR_BATCH_MANUAL_SCORE(game, playtype)], + classes: optNull(PR_BATCH_MANUAL_CLASSES(game)), }); /** @@ -244,12 +245,12 @@ export function ParseBatchManualFromObject( object: unknown, // eslint-disable-next-line @typescript-eslint/no-unused-vars importType: ImportTypes, - logger: KtLogger + _logger: KtLogger ): ParserFunctionReturns { // now to perform some basic validation so we can return // the iterable - if (typeof object !== "object" || object === null) { + if (!IsRecord(object)) { throw new ScoreImportFatalError( 400, `Invalid BATCH-MANUAL (Not an object, received ${ @@ -260,59 +261,61 @@ export function ParseBatchManualFromObject( // attempt to retrieve game // @ts-expect-error man. - const possiblyGame = object.meta?.game; + const maybeGame = object.meta?.game as unknown; // @ts-expect-error man. - const possiblyPlaytype = object.meta?.playtype; + const maybePlaytype = object.meta?.playtype as unknown; - if (!possiblyGame) { + if (maybeGame === undefined) { throw new ScoreImportFatalError( 400, `Could not retrieve meta.game - is this valid BATCH-MANUAL?` ); } - if (!possiblyPlaytype) { + if (maybePlaytype === undefined) { throw new ScoreImportFatalError( 400, `Could not retrieve meta.playtype - is this valid BATCH-MANUAL?` ); } - if (!TachiConfig.GAMES.includes(possiblyGame)) { + // maybeGame could be a number or really anything. + // So we have to check that it's both a string and a valid game. + if (!(typeof maybeGame === "string" && IsValidGame(maybeGame))) { throw new ScoreImportFatalError( 400, - `Invalid game ${possiblyGame} - expected any of ${TachiConfig.GAMES.join(", ")}.` + `Invalid game '${maybeGame}' - expected any of ${TachiConfig.GAMES.join(", ")}.` ); } - const game: Game = possiblyGame; + const game: Game = maybeGame; const gameConfig = GetGameConfig(game); - if (!gameConfig.validPlaytypes.includes(possiblyPlaytype)) { + if (!(typeof maybePlaytype === "string" && IsValidPlaytype(game, maybePlaytype))) { throw new ScoreImportFatalError( 400, - `Invalid playtype ${possiblyPlaytype} - expected any of ${gameConfig.validPlaytypes.join( + `Invalid playtype ${maybePlaytype} - expected any of ${gameConfig.validPlaytypes.join( ", " )}.` ); } - const playtype: Playtype = possiblyPlaytype; + const playtype: Playtype = maybePlaytype; // now that we have the game, we can validate this against // the prudence schema for batch-manual. // This mostly works as a sanity check, and doesn't // check things like whether a score is > 100% // or something. - const err = p(object, PR_BatchManual(game, playtype)); + const err = p(object, PR_BATCH_MANUAL(game, playtype)); if (err) { throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid BATCH-MANUAL")); } - const batchManual = object as BatchManual; + const batchManual = object as unknown as BatchManual; return { game, diff --git a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.ts b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.ts index f22fbd5a6..31db12bdb 100644 --- a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.ts +++ b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/converter.ts @@ -82,7 +82,7 @@ const ConvertEamIIDXCSV: ConverterFunction< context.playtype, eamScore.difficulty, context.importVersion - )) as ChartDocument<"iidx:DP" | "iidx:SP">; + )) as ChartDocument<"iidx:DP" | "iidx:SP"> | null; if (!tachiChart) { throw new KTDataNotFoundFailure( @@ -154,7 +154,7 @@ const ConvertEamIIDXCSV: ConverterFunction< scoreMeta: {}, // japan is gmt+9 - timeAchieved: timestamp ? timestamp - NINE_HOURS : null, + timeAchieved: timestamp !== null ? timestamp - NINE_HOURS : null, }; const numBP = Number(eamScore.bp); diff --git a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.test.ts b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.test.ts index 94d1027e6..e49964f9f 100644 --- a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.test.ts +++ b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.test.ts @@ -1,9 +1,9 @@ +import GenericParseEamIIDXCSV, { IIDXCSVParse, ResolveHeaders } from "./parser"; +import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import { MockMulterFile } from "test-utils/mock-multer"; import { TestingIIDXEamusementCSV26, TestingIIDXEamusementCSV27 } from "test-utils/test-data"; -import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; -import GenericParseEamIIDXCSV, { IIDXCSVParse, ResolveHeaders } from "./parser"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.ts b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.ts index 63537fd09..912b887ec 100644 --- a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.ts +++ b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/parser.ts @@ -3,38 +3,39 @@ import { CSVParseError, NaiveCSVParse } from "utils/naive-csv-parser"; import type { ParserFunctionReturns } from "../types"; import type { EamusementScoreData, IIDXEamusementCSVContext, IIDXEamusementCSVData } from "./types"; import type { KtLogger } from "lib/logger/logger"; +import type { integer } from "tachi-common"; -enum EAM_VERSION_NAMES { - "1st&substream" = 1, - "2nd style", - "3rd style", - "4th style", - "5th style", - "6th style", - "7th style", - "8th style", - "9th style", - "10th style", - "IIDX RED", - "HAPPY SKY", - "DistorteD", - "GOLD", - "DJ TROOPERS", - "EMPRESS", - "SIRIUS", - "Resort Anthem", - "Lincle", - "tricoro", - "SPADA", - "PENDUAL", - "copula", - "SINOBUZ", - "CANNON BALLERS", - "Rootage", - "HEROIC VERSE", - "BISTROVER", - "CastHour", -} +const EAM_VERSION_NAMES: Record = { + "1st&substream": 1, + "2nd style": 2, + "3rd style": 3, + "4th style": 4, + "5th style": 5, + "6th style": 6, + "7th style": 7, + "8th style": 8, + "9th style": 9, + "10th style": 10, + "IIDX RED": 11, + "HAPPY SKY": 12, + DistorteD: 13, + GOLD: 14, + "DJ TROOPERS": 15, + EMPRESS: 16, + SIRIUS: 17, + "Resort Anthem": 18, + Lincle: 19, + tricoro: 20, + SPADA: 21, + PENDUAL: 22, + copula: 23, + SINOBUZ: 24, + "CANNON BALLERS": 25, + Rootage: 26, + "HEROIC VERSE": 27, + BISTROVER: 28, + CastHour: 29, +}; const PRE_HV_HEADER_COUNT = 27; const HV_HEADER_COUNT = 41; @@ -122,9 +123,9 @@ export function IIDXCSVParse(csvBuffer: Buffer, logger: KtLogger) { const timestamp = cells[rawHeaders.length - 1]!.trim(); // wtf typescript?? what's the point of enums? - const versionNum = EAM_VERSION_NAMES[version as keyof typeof EAM_VERSION_NAMES]; + const versionNum = EAM_VERSION_NAMES[version!]; - if (!versionNum) { + if (versionNum === undefined) { logger.info(`Invalid/Unsupported EAM_VERSION_NAME ${version}.`); throw new ScoreImportFatalError( 400, @@ -200,9 +201,8 @@ function GenericParseEamIIDXCSV( const lowercaseFilename = fileData.originalname.toLowerCase(); - // prettier pls if ( - !body.assertPlaytypeCorrect && + body.assertPlaytypeCorrect === undefined && ((lowercaseFilename.includes("sp") && playtype === "DP") || (lowercaseFilename.includes("dp") && playtype === "SP")) ) { diff --git a/server/src/lib/score-import/import-types/file/batch-manual/parser.ts b/server/src/lib/score-import/import-types/file/batch-manual/parser.ts index aa9e872e5..21d973ff9 100644 --- a/server/src/lib/score-import/import-types/file/batch-manual/parser.ts +++ b/server/src/lib/score-import/import-types/file/batch-manual/parser.ts @@ -20,10 +20,7 @@ function ParseBatchManual( try { jsonData = JSON.parse(fileData.buffer.toString("utf-8")); } catch (err) { - throw new ScoreImportFatalError( - 400, - `Invalid JSON. (${(err as Error).message ?? "No Error Message Available."})` - ); + throw new ScoreImportFatalError(400, `Invalid JSON. (${(err as Error).message})`); } return ParseBatchManualFromObject(jsonData, "file/batch-manual", logger); diff --git a/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/converter.test.ts b/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/converter.test.ts index 177e2d673..84d64f085 100644 --- a/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/converter.test.ts +++ b/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/converter.test.ts @@ -1,20 +1,20 @@ +import ConvertEamSDVXCSV from "./converter"; 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 ConvertEamSDVXCSV from "./converter"; -import { SDVXEamusementCSVData } from "./types"; +import type { SDVXEamusementCSVData } from "./types"; const logger = CreateLogCtx(__filename); const parsedScore = { - "title": "ALBIDA Powerless Mix", - "difficulty": "ADVANCED", - "level": "10", - "lamp": "EXCESSIVE COMPLETE", - "score": "9310699", - "exscore": "0" + title: "ALBIDA Powerless Mix", + difficulty: "ADVANCED", + level: "10", + lamp: "EXCESSIVE COMPLETE", + score: "9310699", + exscore: "0", }; t.test("#ConvertEamSDVXCSV", (t) => { @@ -40,6 +40,7 @@ t.test("#ConvertEamSDVXCSV", (t) => { scoreData: { score: 9310699, lamp: "EXCESSIVE CLEAR", + // percent: 93.10699, floating point grade: "AA", judgements: {}, diff --git a/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/parser.test.ts b/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/parser.test.ts index ebe6eb2cf..f47964aea 100644 --- a/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/parser.test.ts +++ b/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/parser.test.ts @@ -1,10 +1,10 @@ +import ParseEamusementSDVXCSV from "./parser"; import CreateLogCtx from "lib/logger/logger"; import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error"; import t from "tap"; import { MockMulterFile } from "test-utils/mock-multer"; import { TestingSDVXEamusementCSV } from "test-utils/test-data"; -import ParseEamusementSDVXCSV from "./parser"; -import { SDVXEamusementCSVData } from "./types"; +import type { SDVXEamusementCSVData } from "./types"; const logger = CreateLogCtx(__filename); @@ -16,7 +16,8 @@ t.test("#ParseEamusementSDVXCSV", (t) => { t.equal(game, "sdvx"); - const iterableData = iterable as SDVXEamusementCSVData[]; + const iterableData = iterable as Array; + t.equal(iterableData.length, 204); t.end(); diff --git a/server/src/lib/score-import/import-types/file/mer-iidx/converter.test.ts b/server/src/lib/score-import/import-types/file/mer-iidx/converter.test.ts index f3bb37b0f..3eb68f643 100644 --- a/server/src/lib/score-import/import-types/file/mer-iidx/converter.test.ts +++ b/server/src/lib/score-import/import-types/file/mer-iidx/converter.test.ts @@ -1,3 +1,4 @@ +import { ConvertFileMerIIDX } from "./converter"; import deepmerge from "deepmerge"; import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; @@ -9,29 +10,27 @@ import { Testing511Song, Testing511SPA, } from "test-utils/test-data"; -import { ConvertFileMerIIDX } from "./converter"; -import { MerScore } from "./types"; +import type { MerScore } from "./types"; const logger = CreateLogCtx(__filename); t.test("#ConvertFileMerIIDX", (t) => { t.beforeEach(ResetDBState); - + const MerScore = { - "music_id": 1000, - "play_type": "SINGLE", - "diff_type": "ANOTHER", - "score": 1000, - "miss_count": 21, - "clear_type": "CLEAR", - "update_time": "2021-03-24 07:15:22" + music_id: 1000, + play_type: "SINGLE", + diff_type: "ANOTHER", + score: 1000, + miss_count: 21, + clear_type: "CLEAR", + update_time: "2021-03-24 07:15:22", } as const; function merc(g: Partial = {}) { return ConvertFileMerIIDX(deepmerge(MerScore, g), {}, "file/mer-iidx", logger); } - t.test("Valid Conversion", async (t) => { const res = await ConvertFileMerIIDX(MerScore, {}, "file/mer-iidx", logger); @@ -47,6 +46,7 @@ t.test("#ConvertFileMerIIDX", (t) => { service: "MER", scoreData: { score: 1000, + // percent: 63.61, approximately, fpa impossible. grade: "B", lamp: "CLEAR", @@ -81,6 +81,7 @@ t.test("#ConvertFileMerIIDX", (t) => { service: "MER", scoreData: { score: 1000, + // percent: 63.61, approximately, fpa impossible. grade: "B", lamp: "CLEAR", diff --git a/server/src/lib/score-import/import-types/file/mer-iidx/converter.ts b/server/src/lib/score-import/import-types/file/mer-iidx/converter.ts index 1a869c75c..19b960494 100644 --- a/server/src/lib/score-import/import-types/file/mer-iidx/converter.ts +++ b/server/src/lib/score-import/import-types/file/mer-iidx/converter.ts @@ -70,7 +70,7 @@ export const ConvertFileMerIIDX: ConverterFunction = asyn }, }, scoreMeta: {}, - timeAchieved: timeAchieved ? timeAchieved : null, + timeAchieved: timeAchieved !== null ? timeAchieved : null, }; return { diff --git a/server/src/lib/score-import/import-types/file/mer-iidx/parser.test.ts b/server/src/lib/score-import/import-types/file/mer-iidx/parser.test.ts index 73793a518..a314e87fc 100644 --- a/server/src/lib/score-import/import-types/file/mer-iidx/parser.test.ts +++ b/server/src/lib/score-import/import-types/file/mer-iidx/parser.test.ts @@ -1,10 +1,10 @@ +import { ParseMerIIDX } from "./parser"; import deepmerge from "deepmerge"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import { MockMulterFile } from "test-utils/mock-multer"; import ResetDBState from "test-utils/resets"; import { GetKTDataBuffer, GetKTDataJSON } from "test-utils/test-data"; -import { ParseMerIIDX } from "./parser"; const logger = CreateLogCtx(__filename); @@ -75,26 +75,25 @@ t.test("#ParseMerIIDX", (t) => { t.end(); }); - const baseScore = - { - "music_id": 3007, - "version_id": 3, - "version_name": "3rd style", - "music_name": "Presto", - "play_type": "SINGLE", - "diff_type": "HYPER", - "score": 566, - "score_percent": 56.8273, - "miss_count": 46, - "grade": "B", - "clear_type": "NO PLAY", - "clear_type_id": 0, - "level": 8, - "note": 498, - "play_count": 6, - "update_time": "2019-06-01 19:56:59", - "score_diff_str": "B+12" - }; + const baseScore = { + music_id: 3007, + version_id: 3, + version_name: "3rd style", + music_name: "Presto", + play_type: "SINGLE", + diff_type: "HYPER", + score: 566, + score_percent: 56.8273, + miss_count: 46, + grade: "B", + clear_type: "NO PLAY", + clear_type_id: 0, + level: 8, + note: 498, + play_count: 6, + update_time: "2019-06-01 19:56:59", + score_diff_str: "B+12", + }; t.test("Should throw on invalid play_type", (t) => { t.throws(() => mrfj(deepmerge(baseScore, { play_type: "INVALID" }))); diff --git a/server/src/lib/score-import/import-types/file/mer-iidx/parser.ts b/server/src/lib/score-import/import-types/file/mer-iidx/parser.ts index 52432d145..8c3a57f01 100644 --- a/server/src/lib/score-import/import-types/file/mer-iidx/parser.ts +++ b/server/src/lib/score-import/import-types/file/mer-iidx/parser.ts @@ -6,7 +6,7 @@ import type { MerScore } from "./types"; import type { KtLogger } from "lib/logger/logger"; import type { EmptyObject } from "utils/types"; -const PR_MerIIDX = { +const PR_MER_IIDX = { music_id: p.isPositiveInteger, play_type: p.isIn("SINGLE", "DOUBLE"), diff_type: p.isIn("NORMAL", "HYPER", "ANOTHER", "LEGGENDARIA", "BEGINNER"), @@ -33,7 +33,7 @@ export function ParseMerIIDX( let jsonData; try { - jsonData = JSON.parse(fileData.buffer.toString("utf-8")); + jsonData = JSON.parse(fileData.buffer.toString("utf-8")) as unknown; } catch (err) { logger.info((err as Error).message); throw new ScoreImportFatalError(400, "Invalid JSON."); @@ -45,7 +45,7 @@ export function ParseMerIIDX( // this is because prudence doesn't support top level arrays at the moment. for (let i = 0; i < jsonData.length; i++) { - const err = p(jsonData[i], PR_MerIIDX, {}, { allowExcessKeys: true }); + const err = p(jsonData[i], PR_MER_IIDX, {}, { allowExcessKeys: true }); if (err) { throw new ScoreImportFatalError(400, `${FormatPrError(err)} [Index ${i}]`); diff --git a/server/src/lib/score-import/import-types/file/solid-state-squad/converter.test.ts b/server/src/lib/score-import/import-types/file/solid-state-squad/converter.test.ts index dc6052755..2c519b60b 100644 --- a/server/src/lib/score-import/import-types/file/solid-state-squad/converter.test.ts +++ b/server/src/lib/score-import/import-types/file/solid-state-squad/converter.test.ts @@ -1,3 +1,4 @@ +import { ConvertFileS3, ParseDifficulty, ResolveS3Lamp } from "./converter"; import deepmerge from "deepmerge"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; @@ -9,8 +10,7 @@ import { Testing511Song, Testing511SPA, } from "test-utils/test-data"; -import { ConvertFileS3, ParseDifficulty, ResolveS3Lamp } from "./converter"; -import { S3Score } from "./types"; +import type { S3Score } from "./types"; const logger = CreateLogCtx(__filename); @@ -49,11 +49,11 @@ t.test("#ConvertFileS3", (t) => { hitMeta: {}, }, scoreMeta: {}, + // can't be tested because the timestamp format doesnt specify a timezone. // timeAchieved: 1287460462000, }; - t.test("Should import a valid S3 score", async (t) => { const res = await cfile(MockParsedS3Score); @@ -96,6 +96,7 @@ t.test("#ConvertFileS3", (t) => { { chart: { songID: 97, difficulty: "HYPER", playtype: "SP" }, song: { title: "ABSOLUTE" }, + // dryScore, dont care }, "Should correctly return the song, chart and DryScore." diff --git a/server/src/lib/score-import/import-types/file/solid-state-squad/converter.ts b/server/src/lib/score-import/import-types/file/solid-state-squad/converter.ts index f4d3bb1c0..16ad8fc6f 100644 --- a/server/src/lib/score-import/import-types/file/solid-state-squad/converter.ts +++ b/server/src/lib/score-import/import-types/file/solid-state-squad/converter.ts @@ -12,8 +12,7 @@ import { FindSongOnTitleInsensitive } from "utils/queries/songs"; import type { DryScore } from "../../../framework/common/types"; import type { ConverterFunction } from "../../common/types"; import type { S3Score } from "./types"; -import type { KtLogger } from "lib/logger/logger"; -import type { Difficulties, Grades, Lamps, Playtypes } from "tachi-common"; +import type { Difficulties, GPTSupportedVersions, Lamps, Playtypes } from "tachi-common"; import type { EmptyObject } from "utils/types"; export function ParseDifficulty(diff: S3Score["diff"]): { @@ -44,35 +43,35 @@ export function ParseDifficulty(diff: S3Score["diff"]): { } } -export function ResolveS3Lamp(data: S3Score, logger: KtLogger): Lamps["iidx:DP" | "iidx:SP"] { - if (data.cleartype === "played") { - return "FAILED"; - } else if (data.cleartype === "cleared") { - if (!data.mods.hardeasy) { - return "CLEAR"; - } else if (data.mods.hardeasy === "H") { - return "HARD CLEAR"; - } else if (data.mods.hardeasy === "E") { - return "EASY CLEAR"; - } +export function ResolveS3Lamp(data: S3Score): Lamps["iidx:DP" | "iidx:SP"] { + switch (data.cleartype) { + case "played": + return "FAILED"; + case "cleared": + switch (data.mods.hardeasy) { + case "E": + return "EASY CLEAR"; + case "H": + return "HARD CLEAR"; + case undefined: + return "CLEAR"; + default: + throw new InvalidScoreFailure( + `Invalid hardeasy of ${data.mods.hardeasy} while evaluating a 'cleared' score?` + ); + } - logger.warn(`Invalid cleartype of 'cleared' with hardeasy of ${data.mods.hardeasy}?`); - throw new InvalidScoreFailure( - `Invalid cleartype of 'cleared' with hardeasy of ${data.mods.hardeasy}?` - ); - } else if ( - data.cleartype === "combo" || - data.cleartype === "comboed" || - data.cleartype === "perfect" || - data.cleartype === "perfected" - ) { - return "FULL COMBO"; + case "combo": + case "comboed": + case "perfect": + case "perfected": + return "FULL COMBO"; + default: + throw new InvalidScoreFailure(`Invalid cleartype of ${data.cleartype}.`); } - - throw new InvalidScoreFailure(`Invalid cleartype of ${data.cleartype}.`); } -const S3_VERSION_CONV = { +const S3_VERSION_CONV: Record = { "3rd": "3-cs", "4th": "4-cs", "5th": "5-cs", @@ -89,27 +88,31 @@ const S3_VERSION_CONV = { emp: "16-cs", pb: "16-cs", us: "bmus", -} as const; +}; function ConvertVersion(joinedStyles: string) { const styles = joinedStyles.split(","); const style = styles[styles.length - 1]; - const convertedStyle = S3_VERSION_CONV[style as keyof typeof S3_VERSION_CONV]; + if (!style) { + throw new InvalidScoreFailure(`Song has invalid style -- Score has no styles?`); + } - if (!convertedStyle) { + const maybeConvertedStyle = S3_VERSION_CONV[style]; + + if (!maybeConvertedStyle) { throw new InvalidScoreFailure(`Song has invalid style ${style}.`); } - return convertedStyle; + return maybeConvertedStyle; } export const ConvertFileS3: ConverterFunction = async ( data, context, importType, - logger + _logger ) => { const song = await FindSongOnTitleInsensitive("iidx", data.songname); @@ -138,7 +141,7 @@ export const ConvertFileS3: ConverterFunction = async ( const { percent, grade } = GenericGetGradeAndPercent("iidx", data.exscore, chart); - const lamp = ResolveS3Lamp(data, logger); + const lamp = ResolveS3Lamp(data); const timeAchieved = ParseDateFromString(data.date); diff --git a/server/src/lib/score-import/import-types/file/solid-state-squad/parser.test.ts b/server/src/lib/score-import/import-types/file/solid-state-squad/parser.test.ts index 03e35637e..190dd8e7d 100644 --- a/server/src/lib/score-import/import-types/file/solid-state-squad/parser.test.ts +++ b/server/src/lib/score-import/import-types/file/solid-state-squad/parser.test.ts @@ -1,9 +1,9 @@ +import { ParseSolidStateXML } from "./parser"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import { MockMulterFile } from "test-utils/mock-multer"; import ResetDBState from "test-utils/resets"; import { GetKTDataBuffer } from "test-utils/test-data"; -import { ParseSolidStateXML } from "./parser"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/import-types/file/solid-state-squad/parser.ts b/server/src/lib/score-import/import-types/file/solid-state-squad/parser.ts index 4fb323eb7..e66c340e0 100644 --- a/server/src/lib/score-import/import-types/file/solid-state-squad/parser.ts +++ b/server/src/lib/score-import/import-types/file/solid-state-squad/parser.ts @@ -8,7 +8,7 @@ import type { KtLogger } from "lib/logger/logger"; import type { PrudenceSchema } from "prudence"; import type { EmptyObject } from "utils/types"; -const PR_SolidState: PrudenceSchema = { +const PR_SOLID_STATE: PrudenceSchema = { s3data: { userdata: { id: p.isPositiveInteger, @@ -61,6 +61,9 @@ export function ParseSolidStateXML( let parsedXML; try { + // insanely hacky. This XML parser might return anything, but we still need + // to traverse it as if it's normal! + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment parsedXML = xmlParser.parse(fileData.buffer.toString("utf-8")); } catch (err) { logger.info("S3 XML Parse Error", err); @@ -68,17 +71,32 @@ export function ParseSolidStateXML( throw new ScoreImportFatalError(400, "Could not parse XML."); } - if (!parsedXML?.s3data?.scoredata?.song) { + // the XML parser can't understand this is meant to be an array + // if someone only has one score, so we need to override it in that case. + // However, this results in us having to do some painful by-hand validation. + // :( + + // @hack Poor typechecking here, but it's kind of painful any way you slice it. + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + let maybeSongs: unknown = parsedXML?.s3data?.scoredata?.song; + + if (maybeSongs === undefined) { throw new ScoreImportFatalError(400, `Invalid S3 XML, no s3data -> scoredata -> song?`); } - // the XML parser can't understand this is meant to be an array - // if someone only has one score. - if (!Array.isArray(parsedXML.s3data.scoredata.song)) { - parsedXML.s3data.scoredata.song = [parsedXML.s3data.scoredata.song]; + // If maybeSongs isn't an array, convert it to an array with one element. + // Note that this is **before** we actually validate it's contents! So even if + // this results in something stupid like [null], we still reject it in validation. + if (!Array.isArray(maybeSongs)) { + maybeSongs = [maybeSongs]; } - const err = p(parsedXML, PR_SolidState, {}, { allowExcessKeys: true }); + // Our validator checks the entire XML, lets mutate the original object + // (which we know to exist now) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + parsedXML.s3data.scoredata.song = maybeSongs; + + const err = p(parsedXML, PR_SOLID_STATE, {}, { allowExcessKeys: true }); if (err) { throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid S3 XML.")); @@ -87,7 +105,7 @@ export function ParseSolidStateXML( return { classHandler: null, context: {}, - iterable: parsedXML.s3data.scoredata.song as Array, + iterable: maybeSongs as Array, game: "iidx", }; } diff --git a/server/src/lib/score-import/import-types/ir/barbatos/converter.test.ts b/server/src/lib/score-import/import-types/ir/barbatos/converter.test.ts index a8de3b200..ab8f2e3a0 100644 --- a/server/src/lib/score-import/import-types/ir/barbatos/converter.test.ts +++ b/server/src/lib/score-import/import-types/ir/barbatos/converter.test.ts @@ -1,11 +1,11 @@ +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 { MockBarbatosScore } from "test-utils/test-data"; -import { ConverterIRBarbatos } from "./converter"; -import { BarbatosScore } from "./types"; +import type { BarbatosScore } from "./types"; const logger = CreateLogCtx(__filename); @@ -55,6 +55,7 @@ t.test("#ConverterIRBarbatos", (t) => { service: "Barbatos", comment: null, importType: "ir/barbatos", + // timeAchieved: , its Date.now() give or take lol scoreData: { score: 9000000, @@ -100,10 +101,12 @@ t.test("#ConverterIRBarbatos", (t) => { }); t.test("Should throw InternalFailure if song-chart desync.", async (t) => { - await db.songs.sdvx.remove({ id: 1 }); // force a song-chart desync + // force a song-chart desync + await db.songs.sdvx.remove({ id: 1 }); t.rejects( - () => ConverterIRBarbatos(MockBarbatosScore, { timeReceived: 10 }, "ir/barbatos", logger), + () => + ConverterIRBarbatos(MockBarbatosScore, { timeReceived: 10 }, "ir/barbatos", logger), { message: /Song 1 \(sdvx\) has no parent song/u, } diff --git a/server/src/lib/score-import/import-types/ir/barbatos/converter.ts b/server/src/lib/score-import/import-types/ir/barbatos/converter.ts index 4fe532ce7..39346b33d 100644 --- a/server/src/lib/score-import/import-types/ir/barbatos/converter.ts +++ b/server/src/lib/score-import/import-types/ir/barbatos/converter.ts @@ -22,7 +22,9 @@ const DIFFICULTY_LOOKUP = { 0: "NOV", 1: "ADV", 2: "EXH", - 3: "ANY_INF", // special case for inf/grv/hvn/vvd - which are all the same diff internally. (kinda). + + // special case for inf/grv/hvn/vvd - which are all the same diff internally. (kinda). + 3: "ANY_INF", 4: "MXM", }; diff --git a/server/src/lib/score-import/import-types/ir/barbatos/parser.test.ts b/server/src/lib/score-import/import-types/ir/barbatos/parser.test.ts index 16d9179d1..82d16137d 100644 --- a/server/src/lib/score-import/import-types/ir/barbatos/parser.test.ts +++ b/server/src/lib/score-import/import-types/ir/barbatos/parser.test.ts @@ -1,8 +1,8 @@ +import { ParseBarbatosSingle } from "./parser"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import ResetDBState from "test-utils/resets"; import { MockBarbatosScore } from "test-utils/test-data"; -import { ParseBarbatosSingle } from "./parser"; const logger = CreateLogCtx(__filename); @@ -10,7 +10,10 @@ t.test("#ParseBarbatosSingle", (t) => { t.beforeEach(ResetDBState); t.test("Should return the score as a payload", (t) => { - const res = ParseBarbatosSingle(MockBarbatosScore as unknown as Record, logger); + const res = ParseBarbatosSingle( + MockBarbatosScore as unknown as Record, + logger + ); t.hasStrict(res, { game: "sdvx", diff --git a/server/src/lib/score-import/import-types/ir/barbatos/parser.ts b/server/src/lib/score-import/import-types/ir/barbatos/parser.ts index c97313b40..12ca598c8 100644 --- a/server/src/lib/score-import/import-types/ir/barbatos/parser.ts +++ b/server/src/lib/score-import/import-types/ir/barbatos/parser.ts @@ -5,7 +5,7 @@ import type { ParserFunctionReturns } from "../../common/types"; import type { BarbatosContext, BarbatosScore } from "./types"; import type { KtLogger } from "lib/logger/logger"; -const PR_Barbatos = { +const PR_BARBATOS = { difficulty: p.isIn(1, 2, 3, 4), level: p.isBoundedInteger(1, 20), song_id: p.isPositiveInteger, @@ -25,9 +25,9 @@ const PR_Barbatos = { export function ParseBarbatosSingle( body: Record, - logger: KtLogger + _logger: KtLogger ): ParserFunctionReturns { - const err = p(body, PR_Barbatos); + const err = p(body, PR_BARBATOS); if (err) { throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid Barbatos Request")); diff --git a/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts b/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts index 3c51cad43..de88f0440 100644 --- a/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts +++ b/server/src/lib/score-import/import-types/ir/beatoraja/converter.ts @@ -196,8 +196,8 @@ export const ConverterIRBeatoraja: ConverterFunction, userID: integer, - logger: KtLogger + _logger: KtLogger ): ParserFunctionReturns { const err = p( body.score, - PR_BeatorajaScore, + PR_BEATORAJA_SCORE, {}, { allowExcessKeys: true, throwOnNonObject: false } ); @@ -85,7 +85,7 @@ export function ParseBeatorajaSingle( const chartErr = p( body.chart, - PR_BeatorajaChart, + PR_BEATORAJA_CHART, {}, { allowExcessKeys: true, throwOnNonObject: false } ); diff --git a/server/src/lib/score-import/import-types/ir/beatoraja/types.ts b/server/src/lib/score-import/import-types/ir/beatoraja/types.ts index 5b5cba81d..d2502d992 100644 --- a/server/src/lib/score-import/import-types/ir/beatoraja/types.ts +++ b/server/src/lib/score-import/import-types/ir/beatoraja/types.ts @@ -3,7 +3,9 @@ import type { integer } from "tachi-common"; export interface BeatorajaContext { client: "beatoraja" | "lr2oraja"; chart: BeatorajaChart; - userID: integer; // unexpectedly necessary for orphan code! + + // unexpectedly necessary for orphan code! + userID: integer; timeReceived: number; } diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.test.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.test.ts index 804b45938..55bc6ae83 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.test.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.test.ts @@ -1,8 +1,8 @@ +import { CreateFerStaticClassHandler } from "./class-handler"; import CreateLogCtx from "lib/logger/logger"; -import { Playtype } from "tachi-common"; import t from "tap"; import ResetDBState from "test-utils/resets"; -import { CreateFerStaticClassHandler } from "./class-handler"; +import type { Playtype } from "tachi-common"; const logger = CreateLogCtx(__filename); 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 46c840975..fcc6d2aec 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 @@ -1,8 +1,8 @@ +import { ParseFervidexStatic } from "./parser"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import ResetDBState from "test-utils/resets"; import { FervidexStaticBase, GetKTDataJSON } from "test-utils/test-data"; -import { ParseFervidexStatic } from "./parser"; const logger = CreateLogCtx(__filename); @@ -10,7 +10,11 @@ t.test("#ParseFervidexStatic", (t) => { t.beforeEach(ResetDBState); t.test("Should parse static data from body", (t) => { - const res = ParseFervidexStatic(FervidexStaticBase, { model: "LDJ:J:B:A:2020092900" }, logger); + const res = ParseFervidexStatic( + FervidexStaticBase, + { model: "LDJ:J:B:A:2020092900" }, + logger + ); t.strictSame(res.iterable, [ { 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 2f3414cff..0865ea4d9 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 @@ -3,6 +3,7 @@ import { AssertStrAsPositiveInt } from "../../../framework/common/string-asserts import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; import { SoftwareIDToVersion } from "../fervidex/parser"; 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"; @@ -10,7 +11,7 @@ import type { FervidexStaticContext, FervidexStaticScore } from "./types"; import type { KtLogger } from "lib/logger/logger"; import type { PrudenceSchema } from "prudence"; -const PR_FervidexStatic: PrudenceSchema = { +const PR_FERVIDEX_STATIC: PrudenceSchema = { ex_score: p.isPositiveInteger, miss_count: p.optional(p.nullable(p.or(p.isPositiveInteger, p.is(-1)))), clear_type: p.isBoundedInteger(0, 7), @@ -25,26 +26,21 @@ export function ParseFervidexStatic( const staticScores = body.scores; - if (!staticScores || typeof staticScores !== "object") { + if (!IsRecord(staticScores)) { throw new ScoreImportFatalError(400, `Invalid body.scores.`); } const scores: Array = []; - for (const songID in staticScores) { - // @ts-expect-error pls. - const subScores = staticScores[songID]; + for (const [songID, subScores] of Object.entries(staticScores)) { + const intSongID = AssertStrAsPositiveInt(songID, `Invalid songID ${songID}.`); - const song_id = AssertStrAsPositiveInt(songID, `Invalid songID ${songID}.`); - - if (!subScores || typeof subScores !== "object") { + if (!IsRecord(subScores)) { throw new ScoreImportFatalError(400, `Invalid score with songID ${songID}.`); } - for (const chart in subScores) { - const score = subScores[chart]; - - if (!score || typeof subScores !== "object") { + for (const [chart, score] of Object.entries(subScores)) { + if (!IsRecord(score)) { throw new ScoreImportFatalError( 400, `Invalid score with songID ${songID} at chart ${chart}.` @@ -55,7 +51,7 @@ export function ParseFervidexStatic( throw new ScoreImportFatalError(400, `Invalid chart ${chart}.`); } - const err = p(score, PR_FervidexStatic); + const err = p(score, PR_FERVIDEX_STATIC); if (err) { throw new ScoreImportFatalError( @@ -64,14 +60,17 @@ export function ParseFervidexStatic( ); } - scores.push({ - song_id, + // is asserted by prudence. + const sc = score as unknown as FervidexStaticScore; - // is asserted with the above check + scores.push({ + song_id: intSongID, + + // is asserted with the above "spb"... check chart: chart as FervidexStaticScore["chart"], - clear_type: score.clear_type, - ex_score: score.ex_score, - miss_count: score.miss_count === undefined ? null : score.miss_count, + clear_type: sc.clear_type, + ex_score: sc.ex_score, + miss_count: sc.miss_count === undefined ? null : sc.miss_count, }); } } diff --git a/server/src/lib/score-import/import-types/ir/fervidex/converter.test.ts b/server/src/lib/score-import/import-types/ir/fervidex/converter.test.ts index 6aa915961..0fb40d92d 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex/converter.test.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex/converter.test.ts @@ -1,10 +1,3 @@ -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 { GetKTDataJSON, Testing511Song, Testing511SPA } from "test-utils/test-data"; -import { InternalFailure, InvalidScoreFailure } from "../../../framework/common/converter-failures"; import { ConverterIRFervidex, SplitFervidexChartRef, @@ -13,7 +6,14 @@ import { TachifyRandom, TachifyRange, } from "./converter"; -import { FervidexScore } from "./types"; +import { InternalFailure } from "../../../framework/common/converter-failures"; +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 { Testing511Song, Testing511SPA } from "test-utils/test-data"; +import type { FervidexScore } from "./types"; const logger = CreateLogCtx(__filename); @@ -82,40 +82,34 @@ t.test("#TachifyRandom", (t) => { }); const baseFervidexScore: FervidexScore = { - "bad": 0, - "chart": "spa", - "clear_type": 1, - "combo_break": 6, - "custom": false, - "chart_sha256": "asdfasdf", - "entry_id": 1000, - "ex_score": 68, - "fast": 0, - "gauge": [ - 100, - 50 - ], - "ghost": [ - 0, - 2 - ], - "good": 0, - "great": 0, - "max_combo": 34, - "option": { - "gauge": "HARD", - "range": "SUDDEN_PLUS", - "style": "RANDOM" + bad: 0, + chart: "spa", + clear_type: 1, + combo_break: 6, + custom: false, + chart_sha256: "asdfasdf", + entry_id: 1000, + ex_score: 68, + fast: 0, + gauge: [100, 50], + ghost: [0, 2], + good: 0, + great: 0, + max_combo: 34, + option: { + gauge: "HARD", + range: "SUDDEN_PLUS", + style: "RANDOM", }, - "pacemaker": { - "name": "", - "score": 363, - "type": "PACEMAKER_A" + pacemaker: { + name: "", + score: 363, + type: "PACEMAKER_A", }, - "pgreat": 34, - "poor": 6, - "slow": 0 -} + pgreat: 34, + poor: 6, + slow: 0, +}; const baseDryScore = { game: "iidx", @@ -249,7 +243,8 @@ t.test("#ConverterIRFervidex", (t) => { }); t.test("Should throw internal failure on chart-song desync", async (t) => { - await db.songs.iidx.remove({}); // this forces desync + // this forces desync + await db.songs.iidx.remove({}); t.rejects( ConverterIRFervidex( @@ -267,6 +262,7 @@ t.test("#ConverterIRFervidex", (t) => { t.test("Should throw invalid score on percent > 100.", (t) => { t.rejects( ConverterIRFervidex( + // eslint-disable-next-line lines-around-comment // @ts-expect-error eternally broken deepmerge deepmerge(baseFervidexScore, { ex_score: 9999 }), { version: "27" }, @@ -282,6 +278,7 @@ t.test("#ConverterIRFervidex", (t) => { t.test("Should throw invalid score on gauge > 100.", (t) => { t.rejects( ConverterIRFervidex( + // eslint-disable-next-line lines-around-comment // @ts-expect-error eternally broken deepmerge deepmerge(baseFervidexScore, { gauge: [150] }), { version: "27" }, diff --git a/server/src/lib/score-import/import-types/ir/fervidex/parser.test.ts b/server/src/lib/score-import/import-types/ir/fervidex/parser.test.ts index 3e2a65758..2af8e24b0 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex/parser.test.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex/parser.test.ts @@ -1,6 +1,6 @@ +import { SoftwareIDToVersion } from "./parser"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; -import { SoftwareIDToVersion } from "./parser"; const logger = CreateLogCtx(__filename); 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 07493bb8f..0866f5c9a 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 @@ -17,7 +17,7 @@ import type { FervidexContext, FervidexScore } from "./types"; import type { KtLogger } from "lib/logger/logger"; import type { PrudenceSchema, ValidSchemaValue } from "prudence"; -const PR_Fervidex: PrudenceSchema = { +const PR_FERVIDEX: PrudenceSchema = { chart: p.isIn("spb", "spn", "dpn", "sph", "dph", "spa", "dpa", "spl", "dpl"), entry_id: p.isPositiveInteger, chart_sha256: "*string", @@ -120,7 +120,7 @@ export function ParseFervidexSingle( const version = SoftwareIDToVersion(headers.model, logger); // more mods may be added in the future, so lets ignore excess keys. - const err = p(body, PR_Fervidex, undefined, { allowExcessKeys: true }); + const err = p(body, PR_FERVIDEX, undefined, { allowExcessKeys: true }); if (err) { throw new ScoreImportFatalError(400, FormatPrError(err, "Invalid Fervidex Request?")); 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 ce13f6d81..4c2bdf9c2 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 @@ -48,7 +48,9 @@ export interface FervidexScore { pacemaker: { name?: string | null; score?: integer | null; - type?: string | null; // too lazy to type this and we dont use it. + + // too lazy to type this properly (it's a string enum) but we dont use it. + type?: string | null; }; "2dx-gsm"?: { diff --git a/server/src/lib/score-import/import-types/ir/kshook-sv6c/converter.test.ts b/server/src/lib/score-import/import-types/ir/kshook-sv6c/converter.test.ts index 880a2f98e..564fb0246 100644 --- a/server/src/lib/score-import/import-types/ir/kshook-sv6c/converter.test.ts +++ b/server/src/lib/score-import/import-types/ir/kshook-sv6c/converter.test.ts @@ -1,8 +1,8 @@ +import { ConverterIRKsHookSV6C } from "./converter"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import ResetDBState from "test-utils/resets"; import { TestingKsHookSV6CScore } from "test-utils/test-data"; -import { ConverterIRKsHookSV6C } from "./converter"; const logger = CreateLogCtx(__filename); @@ -54,7 +54,7 @@ t.test("#ConverterIRKsHookSV6C", (t) => { t.rejects( () => ConverterIRKsHookSV6C( - Object.assign({}, TestingKsHookSV6CScore, { music_id: 10000 }), + { ...TestingKsHookSV6CScore, music_id: 10000 }, { timeReceived: 10 }, "ir/kshook-sv6c", logger diff --git a/server/src/lib/score-import/import-types/ir/kshook-sv6c/parser.test.ts b/server/src/lib/score-import/import-types/ir/kshook-sv6c/parser.test.ts index 8ff787bf2..436bbd1d6 100644 --- a/server/src/lib/score-import/import-types/ir/kshook-sv6c/parser.test.ts +++ b/server/src/lib/score-import/import-types/ir/kshook-sv6c/parser.test.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { ParseKsHookSV6C } from "./parser"; import deepmerge from "deepmerge"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import { TestingKsHookSV6CScore } from "test-utils/test-data"; -import { ParseKsHookSV6C } from "./parser"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/import-types/ir/kshook-sv6c/parser.ts b/server/src/lib/score-import/import-types/ir/kshook-sv6c/parser.ts index fead42636..c132eff9c 100644 --- a/server/src/lib/score-import/import-types/ir/kshook-sv6c/parser.ts +++ b/server/src/lib/score-import/import-types/ir/kshook-sv6c/parser.ts @@ -6,7 +6,7 @@ import type { KsHookSV6CContext, KsHookSV6CScore } from "./types"; import type { KtLogger } from "lib/logger/logger"; import type { PrudenceSchema } from "prudence"; -const PR_KsHookSV6C: PrudenceSchema = { +const PR_KSHOOK_SV6C: PrudenceSchema = { clear: p.isIn( "CLEAR_PLAYED", "CLEAR_EFFECTIVE", @@ -52,10 +52,10 @@ const PR_KsHookSV6C: PrudenceSchema = { export function ParseKsHookSV6C( body: Record, - logger: KtLogger + _logger: KtLogger ): ParserFunctionReturns { // Ignore excess keys, as SV6C might add more features in the future. - const err = p(body, PR_KsHookSV6C, undefined, { allowExcessKeys: true }); + const err = p(body, PR_KSHOOK_SV6C, undefined, { allowExcessKeys: true }); if (err) { throw new ScoreImportFatalError(400, FormatPrError(err)); diff --git a/server/src/lib/score-import/import-types/ir/lr2hook/converter.test.ts b/server/src/lib/score-import/import-types/ir/lr2hook/converter.test.ts index ec70ad485..6936460f4 100644 --- a/server/src/lib/score-import/import-types/ir/lr2hook/converter.test.ts +++ b/server/src/lib/score-import/import-types/ir/lr2hook/converter.test.ts @@ -1,8 +1,8 @@ +import { ConverterLR2Hook } from "./converter"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import ResetDBState from "test-utils/resets"; import { TestingLR2HookScore } from "test-utils/test-data"; -import { ConverterLR2Hook } from "./converter"; const logger = CreateLogCtx(__filename); @@ -46,7 +46,7 @@ t.test("#ConverterLR2Hook", (t) => { t.rejects( () => ConverterLR2Hook( - Object.assign({}, TestingLR2HookScore, { md5: "nonsense_md5" }), + { ...TestingLR2HookScore, md5: "nonsense_md5" }, { timeReceived: 10 }, "ir/lr2hook", logger diff --git a/server/src/lib/score-import/import-types/ir/lr2hook/parser.test.ts b/server/src/lib/score-import/import-types/ir/lr2hook/parser.test.ts index d10994db7..f287ab87f 100644 --- a/server/src/lib/score-import/import-types/ir/lr2hook/parser.test.ts +++ b/server/src/lib/score-import/import-types/ir/lr2hook/parser.test.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import CreateLogCtx from "lib/logger/logger"; -import t from "tap"; import { ParseLR2Hook } from "./parser"; import deepmerge from "deepmerge"; +import CreateLogCtx from "lib/logger/logger"; +import t from "tap"; import { TestingLR2HookScore } from "test-utils/test-data"; import { ApplyNTimes } from "utils/misc"; diff --git a/server/src/lib/score-import/import-types/ir/lr2hook/parser.ts b/server/src/lib/score-import/import-types/ir/lr2hook/parser.ts index f5e718029..f7f5b02ae 100644 --- a/server/src/lib/score-import/import-types/ir/lr2hook/parser.ts +++ b/server/src/lib/score-import/import-types/ir/lr2hook/parser.ts @@ -8,7 +8,7 @@ import type { PrudenceSchema } from "prudence"; const SUPPORTED_RANDOMS = ["NORAN", "MIRROR", "RAN", "S-RAN"]; -export const PR_LR2Hook: PrudenceSchema = { +export const PR_LR2HOOK: PrudenceSchema = { md5: "string", playerData: { autoScr: p.is(0), @@ -53,12 +53,12 @@ export const PR_LR2Hook: PrudenceSchema = { export function ParseLR2Hook( body: Record, - logger: KtLogger + _logger: KtLogger ): ParserFunctionReturns { // Ignore excess keys, as lr2hook is likely to add more features in the future. const err = p( body, - PR_LR2Hook, + PR_LR2HOOK, { playerData: { autoScr: diff --git a/server/src/lib/score-import/import-types/ir/usc/converter.test.ts b/server/src/lib/score-import/import-types/ir/usc/converter.test.ts index ed59e4350..cc7a7033b 100644 --- a/server/src/lib/score-import/import-types/ir/usc/converter.test.ts +++ b/server/src/lib/score-import/import-types/ir/usc/converter.test.ts @@ -1,11 +1,11 @@ -import t from "tap"; -import db from "external/mongo/db"; import { ConverterIRUSC, DeriveLamp, DeriveNoteMod } from "./converter"; import d from "deepmerge"; -import { uscChart, uscScore } from "test-utils/test-data"; +import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; +import t from "tap"; import ResetDBState from "test-utils/resets"; -import { USCClientScore } from "server/router/ir/usc/_playtype/types"; +import { uscChart, uscScore } from "test-utils/test-data"; +import type { USCClientScore } from "server/router/ir/usc/_playtype/types"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/import-types/ir/usc/converter.ts b/server/src/lib/score-import/import-types/ir/usc/converter.ts index 2e09c2a24..346a01a9e 100644 --- a/server/src/lib/score-import/import-types/ir/usc/converter.ts +++ b/server/src/lib/score-import/import-types/ir/usc/converter.ts @@ -16,7 +16,6 @@ import { FindSongOnID } from "utils/queries/songs"; import type { DryScore } from "../../../framework/common/types"; import type { ConverterFunction } from "../../common/types"; import type { IRUSCContext } from "./types"; -import type { KtLogger } from "lib/logger/logger"; import type { USCClientScore } from "server/router/ir/usc/_playtype/types"; import type { Lamps } from "tachi-common"; @@ -38,10 +37,7 @@ export function DeriveNoteMod(data: USCClientScore): "MIR-RAN" | "MIRROR" | "NOR /** * Determines the lamp of a USC score. */ -export function DeriveLamp( - scoreDoc: USCClientScore, - logger: KtLogger -): Lamps["usc:Controller" | "usc:Keyboard"] { +export function DeriveLamp(scoreDoc: USCClientScore): Lamps["usc:Controller" | "usc:Keyboard"] { if (scoreDoc.score === 10_000_000) { return "PERFECT ULTIMATE CHAIN"; } else if (scoreDoc.error === 0) { @@ -50,12 +46,20 @@ export function DeriveLamp( return scoreDoc.gauge >= 0.7 ? "CLEAR" : "FAILED"; } else if (scoreDoc.options.gaugeType === 1) { return scoreDoc.gauge > 0 ? "EXCESSIVE CLEAR" : "FAILED"; - } else if (scoreDoc.options.gaugeType === 2) { - return "FAILED"; } - logger.error(`Could not derive Lamp from Score Document`, { scoreDoc }); - throw new InternalFailure(`Could not derive Lamp from Score Document`); + // else if (scoreDoc.options.gaugeType === 2) + return "FAILED"; +} + +function ConvertGaugeType(gaugeType: 0 | 1 | 2) { + if (gaugeType === 0) { + return "NORMAL"; + } else if (gaugeType === 1) { + return "HARD"; + } + + return "PERMISSIVE"; } export const ConverterIRUSC: ConverterFunction = async ( @@ -114,7 +118,7 @@ export const ConverterIRUSC: ConverterFunction = a grade, percent, score: data.score, - lamp: DeriveLamp(data, logger), + lamp: DeriveLamp(data), judgements: { critical: data.crit, near: data.near, @@ -128,12 +132,7 @@ export const ConverterIRUSC: ConverterFunction = a }, }, scoreMeta: { - gaugeMod: - data.options.gaugeType === 0 - ? "NORMAL" - : data.options.gaugeType === 1 - ? "HARD" - : "PERMISSIVE", + gaugeMod: ConvertGaugeType(data.options.gaugeType), noteMod: DeriveNoteMod(data), }, }; diff --git a/server/src/lib/score-import/import-types/ir/usc/parser.test.ts b/server/src/lib/score-import/import-types/ir/usc/parser.test.ts index 0d5f99485..df640685b 100644 --- a/server/src/lib/score-import/import-types/ir/usc/parser.test.ts +++ b/server/src/lib/score-import/import-types/ir/usc/parser.test.ts @@ -1,7 +1,7 @@ +import { ParseIRUSC } from "./parser"; +import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import { uscChart, uscScore } from "test-utils/test-data"; -import CreateLogCtx from "lib/logger/logger"; -import { ParseIRUSC } from "./parser"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/import-types/ir/usc/parser.ts b/server/src/lib/score-import/import-types/ir/usc/parser.ts index 371747801..73e507e51 100644 --- a/server/src/lib/score-import/import-types/ir/usc/parser.ts +++ b/server/src/lib/score-import/import-types/ir/usc/parser.ts @@ -8,7 +8,7 @@ import type { PrudenceSchema } from "prudence"; import type { USCClientScore } from "server/router/ir/usc/_playtype/types"; import type { Playtypes } from "tachi-common"; -const PR_USCIRScore: PrudenceSchema = { +const PR_USCIR_SCORE: PrudenceSchema = { score: p.isBoundedInteger(0, 10_000_000), gauge: p.isBetween(0, 1), timestamp: p.isPositiveInteger, @@ -37,11 +37,11 @@ export function ParseIRUSC( body: Record, chartHash: string, playtype: Playtypes["usc"], - logger: KtLogger + _logger: KtLogger ): ParserFunctionReturns { const err = p( body.score, - PR_USCIRScore, + PR_USCIR_SCORE, {}, { throwOnNonObject: false, allowExcessKeys: true } ); diff --git a/server/src/lib/score-import/worker/types.ts b/server/src/lib/score-import/worker/types.ts index e6e764b7d..bc1511492 100644 --- a/server/src/lib/score-import/worker/types.ts +++ b/server/src/lib/score-import/worker/types.ts @@ -1,6 +1,6 @@ import type { Parsers } from "../import-types/parsers"; import type { Job } from "bullmq"; -import type { ImportTypes, integer } from "tachi-common"; +import type { ImportDocument, ImportTypes, integer } from "tachi-common"; // Ok so, please hear me out on this one. // We need a type that removes the last element from an array. @@ -42,6 +42,13 @@ export interface ScoreImportProgress { description: string; } -export type ScoreImportWorkerReturns { - -} \ No newline at end of file +export type ScoreImportWorkerReturns = + | { + success: false; + statusCode: integer; + description: string; + } + | { + success: true; + importDocument: ImportDocument; + }; diff --git a/server/src/lib/score-import/worker/worker.ts b/server/src/lib/score-import/worker/worker.ts index 5161b8b6f..f791201b8 100644 --- a/server/src/lib/score-import/worker/worker.ts +++ b/server/src/lib/score-import/worker/worker.ts @@ -8,7 +8,7 @@ import CreateLogCtx from "lib/logger/logger"; import { Environment, ServerConfig } from "lib/setup/config"; import { FormatUserDoc, GetUserWithID } from "utils/user"; import { EventEmitter } from "events"; -import type { ScoreImportJob } from "./types"; +import type { ScoreImportJob, ScoreImportJobData } from "./types"; import type { ImportTypes } from "tachi-common"; EventEmitter.defaultMaxListeners = 20; @@ -64,19 +64,21 @@ export const worker = new Worker( // turned into nonsense objects. We need to "deJSONify" these buffers // so lets do that now. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const processedArgs: any = []; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + const processedArgs: Array = []; + + // eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call for (const arg of job.data.parserArguments as Array) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (arg?.buffer?.type === "Buffer") { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access processedArgs.push({ ...arg, buffer: Buffer.from(arg.buffer.data) }); } else { processedArgs.push(arg); } } - job.data.parserArguments = processedArgs; + job.data.parserArguments = processedArgs as ScoreImportJobData["parserArguments"]; logger.debug(`received score import job ${job.id}`, { job }); @@ -141,5 +143,5 @@ worker.on("completed", (job, result) => { }); process.on("SIGTERM", () => { - HandleSIGTERMGracefully(); + void HandleSIGTERMGracefully(); }); diff --git a/server/src/lib/score-mutation/delete-scores.test.ts b/server/src/lib/score-mutation/delete-scores.test.ts index f3ea16a05..33efe0e84 100644 --- a/server/src/lib/score-mutation/delete-scores.test.ts +++ b/server/src/lib/score-mutation/delete-scores.test.ts @@ -1,10 +1,10 @@ +import { DeleteScore } from "./delete-scores"; import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import { ImportDocument, ScoreDocument, SessionDocument } from "tachi-common"; import t from "tap"; import ResetDBState from "test-utils/resets"; import { TestingIIDXSPScore } from "test-utils/test-data"; -import { DeleteScore } from "./delete-scores"; +import type { ImportDocument, ScoreDocument, SessionDocument } from "tachi-common"; const mockImportDocument: ImportDocument = { userID: 1, diff --git a/server/src/lib/score-mutation/update-score.test.ts b/server/src/lib/score-mutation/update-score.test.ts index 3cfdf3ae6..d20cde497 100644 --- a/server/src/lib/score-mutation/update-score.test.ts +++ b/server/src/lib/score-mutation/update-score.test.ts @@ -1,11 +1,11 @@ +import UpdateScore from "./update-score"; import deepmerge from "deepmerge"; import db from "external/mongo/db"; import { CreateScoreID } from "lib/score-import/framework/score-importing/score-id"; -import { ImportDocument, ScoreDocument, SessionDocument } from "tachi-common"; import t from "tap"; import ResetDBState from "test-utils/resets"; import { TestingIIDXSPScore } from "test-utils/test-data"; -import UpdateScore from "./update-score"; +import type { ImportDocument, ScoreDocument, SessionDocument } from "tachi-common"; const mockImportDocument: ImportDocument = { userID: 1, diff --git a/server/src/lib/search/search.test.ts b/server/src/lib/search/search.test.ts index bca76bcd7..f48ed3ca1 100644 --- a/server/src/lib/search/search.test.ts +++ b/server/src/lib/search/search.test.ts @@ -1,32 +1,32 @@ +import { SearchGameSongs, SearchUsersRegExp } from "./search"; 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"; t.test("#SearchGameSongs", (t) => { t.beforeEach(ResetDBState); t.beforeEach(LoadTachiIIDXData); t.beforeEach(async () => { await db.songs.iidx.dropIndexes(); - await db.songs.iidx.createIndex( - { - title: "text", - artist: "text", - altTitles: "text", - searchTerms: "text", - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any /* known bug with monk */ - ); + + // Casting here is due to a known bug with monk. + await db.songs.iidx.createIndex({ + title: "text", + artist: "text", + altTitles: "text", + searchTerms: "text", + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); }); t.test("Should return songs like the query.", async (t) => { const res = await SearchGameSongs("iidx", "amuro"); + // for simplicity of testing (and because the + // return order is ambiguous) we sort on + // songID here and expect this. t.strictSame( - // for simplicity of testing (and because the - // return order is ambiguous) we sort on - // songID here and expect this. res.sort((a, b) => a.id - b.id).map((e) => e.title), ["A", "AA", "冥", "F", "HAERETICUS", "ZZ", "X", "AA -rebuild-", "∀", "-65℃"] ); diff --git a/server/src/lib/search/search.ts b/server/src/lib/search/search.ts index 4ef7e50ea..670ac1edf 100644 --- a/server/src/lib/search/search.ts +++ b/server/src/lib/search/search.ts @@ -32,37 +32,37 @@ export function SearchCollection( existingMatch: FilterQuery = {}, limit = 100 ): Promise> { - return collection - .aggregate([ - { - $match: deepmerge({ $text: { $search: search } }, existingMatch), + const agg: Promise> = collection.aggregate([ + { + $match: deepmerge({ $text: { $search: search } }, existingMatch), + }, + + // Project the __textScore field on so we can sort + // based on search proximity to query + { + $addFields: { + __textScore: { $meta: "textScore" }, }, + }, - // Project the __textScore field on so we can sort - // based on search proximity to query - { - $addFields: { - __textScore: { $meta: "textScore" }, - }, - }, + // sort by quality of match + { $sort: { __textScore: -1 } }, - // sort by quality of match - { $sort: { __textScore: -1 } }, + // hide nonsense + { $match: { __textScore: { $gt: 0.25 } } }, + { $limit: limit }, + { $unset: "_id" }, + ]); - // hide nonsense - { $match: { __textScore: { $gt: 0.25 } } }, - { $limit: limit }, - { $unset: "_id" }, - ]) - .catch((err) => { - logger.error( - `An error occured while trying to $text search collection ${collection.name}. Does this have a $text index?`, - { err } - ); + return agg.catch((err: unknown) => { + logger.error( + `An error occured while trying to $text search collection ${collection.name}. Does this have a $text index?`, + { err } + ); - // throw it up further - throw err; - }); + // throw it up further + throw err; + }); } export type SongSearchReturn = SongDocument & { @@ -115,7 +115,7 @@ export function SearchSessions( baseMatch.playtype = playtype; } - if (userID) { + if (userID !== undefined) { baseMatch.userID = userID; } @@ -184,12 +184,12 @@ export async function SearchForChartHash(search: string) { db.charts.bms.findOne({ $or: [{ "data.hashMD5": search }, { "data.hashSHA256": search }] }), db.charts.pms.findOne({ $or: [{ "data.hashMD5": search }, { "data.hashSHA256": search }] }), db.charts.usc.findOne({ "data.hashSHA1": search }), - ])) as [ChartDocument, ChartDocument, ChartDocument]; + ])) as [ChartDocument | null, ChartDocument | null, ChartDocument | null]; const [bmsChart, pmsChart, uscChart] = results; const songs = []; - const charts = results.filter((e) => e !== null); + const charts = results.filter((e) => e !== null) as Array; if (bmsChart) { songs.push( diff --git a/server/src/lib/showcase/evaluator.test.ts b/server/src/lib/showcase/evaluator.test.ts index 315f52e65..9295f7abf 100644 --- a/server/src/lib/showcase/evaluator.test.ts +++ b/server/src/lib/showcase/evaluator.test.ts @@ -1,19 +1,20 @@ -import deepmerge from "deepmerge"; -import t from "tap"; import { EvaluateShowcaseStat } from "./evaluator"; - +import deepmerge from "deepmerge"; +import db from "external/mongo/db"; +import { IIDX_LAMPS } from "tachi-common"; +import t from "tap"; import ResetDBState from "test-utils/resets"; import { Testing511SPA, TestingIIDXFolderSP10, TestingIIDXSPScorePB } from "test-utils/test-data"; import { CreateFolderChartLookup } from "utils/folder"; -import db from "external/mongo/db"; -import { IIDX_LAMPS } from "tachi-common"; /* eslint-disable no-return-await */ // causes a race condition otherwise due to weird tap interaction t.test("#EvaluateShowcaseStat", (t) => { t.beforeEach(ResetDBState); - t.beforeEach(async () => await CreateFolderChartLookup(TestingIIDXFolderSP10, true)); + t.beforeEach(async () => { + await CreateFolderChartLookup(TestingIIDXFolderSP10, true); + }); t.beforeEach( async () => await db["personal-bests"].insert(deepmerge(TestingIIDXSPScorePB, {})) ); diff --git a/server/src/lib/showcase/evaluator.ts b/server/src/lib/showcase/evaluator.ts index b7127da0e..09a14f11d 100644 --- a/server/src/lib/showcase/evaluator.ts +++ b/server/src/lib/showcase/evaluator.ts @@ -14,14 +14,16 @@ export function EvaluateShowcaseStat( value: number | null; outOf?: number; }> { - if (details.mode === "chart") { - return EvaluateShowcaseChartStat(details, userID); - } else if (details.mode === "folder") { - return EvaluateShowcaseFolderStat(details, userID); - } + switch (details.mode) { + case "chart": + return EvaluateShowcaseChartStat(details, userID); + case "folder": + return EvaluateShowcaseFolderStat(details, userID); - // @ts-expect-error This should never happen anyway. - throw new Error(`Invalid mode of ${details.mode} as details mode?`); + default: + // @ts-expect-error This should never happen anyway -- this ignore ignores a 'never' result. + throw new Error(`Invalid mode of ${details.mode} as details mode?`); + } } async function EvaluateShowcaseChartStat(details: ShowcaseStatChart, userID: integer) { diff --git a/server/src/lib/showcase/get-related.ts b/server/src/lib/showcase/get-related.ts index a657e18ab..767cd9023 100644 --- a/server/src/lib/showcase/get-related.ts +++ b/server/src/lib/showcase/get-related.ts @@ -5,30 +5,39 @@ import type { Game, ShowcaseStatDetails } from "tachi-common"; const logger = CreateLogCtx(__filename); export async function GetRelatedStatDocuments(stat: ShowcaseStatDetails, game: Game) { - if (stat.mode === "chart") { - const chart = await db.charts[game].findOne({ chartID: stat.chartID }); + switch (stat.mode) { + case "chart": { + const chart = await db.charts[game].findOne({ chartID: stat.chartID }); - if (!chart) { - logger.error(`This stat refers to a chart that does not exist?`, { stat }); - throw new Error(`Stat refers to a chart that does not exist? ${stat.chartID}.`); + if (!chart) { + logger.error(`This stat refers to a chart that does not exist?`, { stat }); + throw new Error(`Stat refers to a chart that does not exist? ${stat.chartID}.`); + } + + const song = await db.songs[game].findOne({ id: chart.songID }); + + if (!song) { + logger.severe(`Song-Chart Mismatch - ${chart.songID}.`, { chart }); + throw new Error(`Song-Chart Mismatch on ${chart.songID}.`); + } + + return { song, chart }; } - const song = await db.songs[game].findOne({ id: chart.songID }); + case "folder": { + const folders = await db.folders.find({ + folderID: { $in: Array.isArray(stat.folderID) ? stat.folderID : [stat.folderID] }, + }); - if (!song) { - logger.severe(`Song-Chart Mismatch - ${chart.songID}.`, { chart }); - throw new Error(`Song-Chart Mismatch on ${chart.songID}.`); + return { folders }; } - return { song, chart }; - } else if (stat.mode === "folder") { - const folders = await db.folders.find({ - folderID: { $in: Array.isArray(stat.folderID) ? stat.folderID : [stat.folderID] }, - }); - - return { folders }; + default: { + logger.error( + `Invalid stat - has nonsense stat.mode of ${(stat as ShowcaseStatDetails).mode}.`, + { stat } + ); + throw new Error(`Invalid stat.mode in stat?`); + } } - - logger.error(`Invalid stat - has nonsense stat.mode.`, { stat }); - throw new Error(`Invalid stat.mode in stat?`); } diff --git a/server/src/lib/showcase/get-stats.test.ts b/server/src/lib/showcase/get-stats.test.ts index 248346ebc..87b182f6a 100644 --- a/server/src/lib/showcase/get-stats.test.ts +++ b/server/src/lib/showcase/get-stats.test.ts @@ -1,12 +1,10 @@ -import t from "tap"; -import db from "external/mongo/db"; - -import ResetDBState from "test-utils/resets"; -import { TestingIIDXFolderSP10, Testing511SPA, TestingIIDXSPScorePB } from "test-utils/test-data"; -import { IIDX_LAMPS } from "tachi-common"; - import { EvaluateUsersStatsShowcase } from "./get-stats"; import deepmerge from "deepmerge"; +import db from "external/mongo/db"; +import { IIDX_LAMPS } from "tachi-common"; +import t from "tap"; +import ResetDBState from "test-utils/resets"; +import { TestingIIDXFolderSP10, Testing511SPA, TestingIIDXSPScorePB } from "test-utils/test-data"; import { CreateFolderChartLookup } from "utils/folder"; t.test("#EvalulateUsersStatsShowcase", (t) => { diff --git a/server/src/lib/showcase/showcase.test.ts b/server/src/lib/showcase/showcase.test.ts index 1d9eb722f..ea30c891f 100644 --- a/server/src/lib/showcase/showcase.test.ts +++ b/server/src/lib/showcase/showcase.test.ts @@ -1,8 +1,8 @@ +import { RemoveStaleFolderShowcaseStats } from "./showcase"; import db from "external/mongo/db"; import t from "tap"; import { mkFakeGameSettings } from "test-utils/misc"; import ResetDBState from "test-utils/resets"; -import { RemoveStaleFolderShowcaseStats } from "./showcase"; t.test("#RemoveStaleFolderShowcaseStats", (t) => { t.beforeEach(ResetDBState); diff --git a/server/src/lib/targets/goal-utils.ts b/server/src/lib/targets/goal-utils.ts index 30af9bb81..dfcfe8cc8 100644 --- a/server/src/lib/targets/goal-utils.ts +++ b/server/src/lib/targets/goal-utils.ts @@ -1,6 +1,6 @@ import db from "external/mongo/db"; import { GenericCalculatePercent } from "lib/score-import/framework/common/score-utils"; -import { FormatGame, GetGamePTConfig, Playtypes } from "tachi-common"; +import { FormatGame, GetGamePTConfig } from "tachi-common"; import { GetFolderForIDGuaranteed, HumaniseChartID } from "utils/db"; import { GetFolderChartIDs } from "utils/folder"; import { FormatMaxDP, HumanisedJoinArray } from "utils/misc"; @@ -29,8 +29,9 @@ export async function CreateGoalTitle( return `${formattedCriteria} any chart in ${datasetName}`; } + // Eslint can't figure out that the above switches are safely exhastive. Ah well. // eslint-disable-next-line no-fallthrough - case "absolute": { + case "absolute": switch (charts.type) { case "any": return `${formattedCriteria} ${criteria.countNum} charts`; @@ -38,13 +39,15 @@ export async function CreateGoalTitle( return `${formattedCriteria} ${criteria.countNum} of ${datasetName}`; case "folder": return `${formattedCriteria} ${criteria.countNum} charts in ${datasetName}`; + case "single": + throw new Error( + `Invalid goal -- absolute mode cannot be paired with a charts.type of 'single'.` + ); } - break; - } - + // See above about switch exhaustivity + // eslint-disable-next-line no-fallthrough case "proportion": { - // eslint-disable-next-line no-case-declarations // I know declaring inside cases is a footgun, but I know better. const propFormat = FormatMaxDP(criteria.countNum * 100); switch (charts.type) { @@ -54,30 +57,43 @@ export async function CreateGoalTitle( return `${formattedCriteria} ${propFormat}% of ${datasetName}`; case "folder": return `${formattedCriteria} ${propFormat}% of the charts in ${datasetName}`; + case "single": + throw new Error( + `Invalid goal -- absolute mode cannot be paired with a charts.type of 'single'.` + ); } } } - - throw new Error(`Couldn't create title from provided info. Is the provided data supported?`); } async function FormatCharts(charts: GoalDocument["charts"], game: Game) { - if (charts.type === "single") { - return HumaniseChartID(game, charts.data); - } else if (charts.type === "multi") { - // @inefficient - // This could be done with significantly less db queries. - const formattedTitles = await Promise.all( - charts.data.map((chartID) => HumaniseChartID(game, chartID)) - ); + switch (charts.type) { + case "single": + return HumaniseChartID(game, charts.data); + case "multi": { + // @inefficient + // This could be done with significantly less db queries. + const formattedTitles = await Promise.all( + charts.data.map((chartID) => HumaniseChartID(game, chartID)) + ); - return HumanisedJoinArray(formattedTitles); - } else if (charts.type === "folder") { - const folder = await GetFolderForIDGuaranteed(charts.data); + return HumanisedJoinArray(formattedTitles); + } - return `the ${folder.title} folder`; - } else if (charts.type === "any") { - return null; + case "folder": { + const folder = await GetFolderForIDGuaranteed(charts.data); + + return `the ${folder.title} folder`; + } + + case "any": + return null; + default: + throw new Error( + `Invalid goal charts.type -- got ${ + (charts as GoalDocument["charts"]).type + }, which we don't support?` + ); } } @@ -101,7 +117,7 @@ function FormatCriteria(criteria: GoalDocument["criteria"], game: Game, playtype * any sense at all. There are certain combinations that are illegal, or values that * in general just should be constrained out. * - * @warn This function is disgusting. + * @warn This function is disgusting. This should have never happened. */ export async function ValidateGoalChartsAndCriteria( charts: GoalDocument["charts"], diff --git a/server/src/lib/targets/goals.test.ts b/server/src/lib/targets/goals.test.ts index 28bae6d0d..02ca668de 100644 --- a/server/src/lib/targets/goals.test.ts +++ b/server/src/lib/targets/goals.test.ts @@ -1,7 +1,7 @@ +import { EvaluateGoalForUser, HumaniseGoalProgress } from "./goals"; import deepmerge from "deepmerge"; import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; -import { GoalDocument } from "tachi-common"; import t from "tap"; import ResetDBState from "test-utils/resets"; import { @@ -12,7 +12,7 @@ import { } from "test-utils/test-data"; import { CreateFolderChartLookup } from "utils/folder"; import { Random20Hex } from "utils/misc"; -import { EvaluateGoalForUser, HumaniseGoalProgress } from "./goals"; +import type { GoalDocument } from "tachi-common"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/targets/goals.ts b/server/src/lib/targets/goals.ts index e428594fa..c296791e9 100644 --- a/server/src/lib/targets/goals.ts +++ b/server/src/lib/targets/goals.ts @@ -80,121 +80,131 @@ export async function EvaluateGoalForUser( scoreQuery.chartID = { $in: chartIDs }; } - // Next, we need to figure out our criteria. - if (goal.criteria.mode === "single") { - const res = await db["personal-bests"].findOne(scoreQuery); + switch (goal.criteria.mode) { + case "single": { + const res = await db["personal-bests"].findOne(scoreQuery); - // hack, but guaranteed to work. - const scoreDataKey = goal.criteria.key.split(".")[1] as - | "gradeIndex" - | "lampIndex" - | "percent" - | "score"; + // hack, but guaranteed to work. + const scoreDataKey = goal.criteria.key.split(".")[1] as + | "gradeIndex" + | "lampIndex" + | "percent" + | "score"; - const outOfHuman = HumaniseGoalProgress( - goal.game, - goal.playtype, - goal.criteria.key, - goal.criteria.value, - null - ); + const outOfHuman = HumaniseGoalProgress( + goal.game, + goal.playtype, + goal.criteria.key, + goal.criteria.value, + null + ); + + if (res) { + return { + achieved: true, + outOf: goal.criteria.value, + progress: res.scoreData[scoreDataKey], + outOfHuman, + progressHuman: HumaniseGoalProgress( + goal.game, + goal.playtype, + goal.criteria.key, + res.scoreData[scoreDataKey], + res + ), + }; + } + + // if we weren't successful, we have to get the users next best score and put it up here + // this is made infinitely easier by the existence of personal-bests. + const nextBestQuery: FilterQuery = { + userID, + game: goal.game, + playtype: goal.playtype, + }; + + if (chartIDs) { + nextBestQuery.chartID = { $in: chartIDs }; + } + + const nextBestScore = await db["personal-bests"].findOne(nextBestQuery, { + sort: { [goal.criteria.key]: -1 }, + }); + + if (!nextBestScore) { + return { + achieved: false, + outOf: goal.criteria.value, + progress: null, + outOfHuman, + progressHuman: "NO DATA", + }; + } - if (res) { return { - achieved: true, + achieved: false, outOf: goal.criteria.value, - progress: res.scoreData[scoreDataKey], outOfHuman, + progress: nextBestScore.scoreData[scoreDataKey], progressHuman: HumaniseGoalProgress( goal.game, goal.playtype, goal.criteria.key, - res.scoreData[scoreDataKey], - res + nextBestScore.scoreData[scoreDataKey], + nextBestScore ), }; } - // if we weren't successful, we have to get the users next best score and put it up here - // this is made infinitely easier by the existence of personal-bests. - const nextBestQuery: FilterQuery = { - userID, - game: goal.game, - playtype: goal.playtype, - }; + case "absolute": + case "proportion": { + let count; - if (chartIDs) { - nextBestQuery.chartID = { $in: chartIDs }; - } + // abs -> Absolute mode, such as clear 10 charts. + if (goal.criteria.mode === "absolute") { + count = goal.criteria.countNum; + } else { + // proportion -> Proportional mode, the value + // is a multiplier for the amount of charts + // available -- i.e. 0.1 * charts. - const nextBestScore = await db["personal-bests"].findOne(nextBestQuery, { - sort: { [goal.criteria.key]: -1 }, - }); + let totalChartCount; + + if (chartIDs === null) { + // edge case: proportion goals on "any" + // charts (i.e. clear 20% of charts) need to + // know how many charts the game has! + totalChartCount = await db.charts[goal.game].count({ playtype: goal.playtype }); + } else { + totalChartCount = chartIDs.length; + } + + count = Math.floor(goal.criteria.countNum * totalChartCount); + } + + const userCount = await db["personal-bests"].count(scoreQuery); - if (!nextBestScore) { return { - achieved: false, - outOf: goal.criteria.value, - progress: null, - outOfHuman, - progressHuman: "NO DATA", + achieved: userCount >= count, + progress: userCount, + outOf: count, + progressHuman: userCount.toString(), + outOfHuman: count.toString(), }; } - return { - achieved: false, - outOf: goal.criteria.value, - outOfHuman, - progress: nextBestScore.scoreData[scoreDataKey], - progressHuman: HumaniseGoalProgress( - goal.game, - goal.playtype, - goal.criteria.key, - nextBestScore.scoreData[scoreDataKey], - nextBestScore - ), - }; - } else if (goal.criteria.mode === "absolute" || goal.criteria.mode === "proportion") { - let count; + default: { + // note that this seemingly nonsensical type assertion is because typescript has whittled down + // goal.criteria (correctly) to 'never', but we want to log if something somehow ends up here (it shouldn't). + logger.warn( + `Invalid goal: ${goal.goalID}, unknown criteria.mode ${ + (goal.criteria as GoalDocument["criteria"]).mode + }, ignoring.` + ); - // abs -> Absolute mode, such as clear 10 charts. - if (goal.criteria.mode === "absolute") { - count = goal.criteria.countNum; - } else { - // proportion -> Proportional mode, the value - // is a multiplier for the amount of charts - // available -- i.e. 0.1 * charts. - - let totalChartCount; - - if (chartIDs === null) { - // edge case: proportion goals on "any" - // charts (i.e. clear 20% of charts) need to - // know how many charts the game has! - totalChartCount = await db.charts[goal.game].count({ playtype: goal.playtype }); - } else { - totalChartCount = chartIDs.length; - } - - count = Math.floor(goal.criteria.countNum * totalChartCount); + return null; } - - const userCount = await db["personal-bests"].count(scoreQuery); - - return { - achieved: userCount >= count, - progress: userCount, - outOf: count, - progressHuman: userCount.toString(), - outOfHuman: count.toString(), - }; } - - logger.error( - `Invalid goal: ${goal.goalID}, unknown criteria.mode ${goal.criteria.mode}, ignoring.` - ); - - return null; } /** @@ -211,10 +221,12 @@ function ResolveGoalCharts( return goal.charts.data; } else if (goal.charts.type === "folder") { return GetFolderChartIDs(goal.charts.data); - } else if (goal.charts.type === "any") { - // special case. - return null; } + + // else if (goal.charts.type === "any") + // this is a special case, null means the set of all charts. + // special case. + return null; } type GoalKeys = GoalDocument["criteria"]["key"]; @@ -284,7 +296,9 @@ export async function ConstructGoal( // Throws if the charts or criteria are invalid somehow. await ValidateGoalChartsAndCriteria(charts, criteria, game, playtype); - return { + // @ts-expect-error It's complaining because the potential criteria types might mismatch. + // they're right, but this is enforced by ValidateGoalChartsAndCriteria. + const goalDocument: GoalDocument = { game, playtype, timeAdded: Date.now(), @@ -292,7 +306,9 @@ export async function ConstructGoal( charts, goalID: CreateGoalID(charts, criteria, game, playtype), name: await CreateGoalName(charts, criteria, game, playtype), - } as GoalDocument; + }; + + return goalDocument; } /** @@ -375,39 +391,40 @@ export function GetMilestonesThatContainGoal(goalID: string) { export async function GetBlockingParentMilestoneSubs( goalSub: GoalSubscriptionDocument ): Promise> { - const blockers = await db["milestone-subs"].aggregate([ - { - // find all milestones that this user is subscribed to - $match: { - userID: goalSub.userID, - game: goalSub.game, - playtype: goalSub.playtype, + const blockers: Array = + await db["milestone-subs"].aggregate([ + { + // find all milestones that this user is subscribed to + $match: { + userID: goalSub.userID, + game: goalSub.game, + playtype: goalSub.playtype, + }, }, - }, - { - // look up the parent milestones - $lookup: { - from: "milestones", - localField: "milestoneID", - foreignField: "milestoneID", - as: "parentMilestoneSubs", + { + // look up the parent milestones + $lookup: { + from: "milestones", + localField: "milestoneID", + foreignField: "milestoneID", + as: "parentMilestoneSubs", + }, }, - }, - { - // then project it onto the $milestone field. This will be null - // if the milestone has no parent, which we hopefully won't have - // to consider (illegal) - $set: { - milestone: { $arrayElemAt: ["$parentMilestoneSubs", 0] }, + { + // then project it onto the $milestone field. This will be null + // if the milestone has no parent, which we hopefully won't have + // to consider (illegal) + $set: { + milestone: { $arrayElemAt: ["$parentMilestoneSubs", 0] }, + }, }, - }, - { - // then finally, filter to only milestones that pertain to this goal. - $match: { - "milestone.milestoneData.goals.goalID": goalSub.goalID, + { + // then finally, filter to only milestones that pertain to this goal. + $match: { + "milestone.milestoneData.goals.goalID": goalSub.goalID, + }, }, - }, - ]); + ]); return blockers; } diff --git a/server/src/lib/targets/milestones.ts b/server/src/lib/targets/milestones.ts index 874332f84..88adf46a2 100644 --- a/server/src/lib/targets/milestones.ts +++ b/server/src/lib/targets/milestones.ts @@ -60,22 +60,30 @@ export async function GetGoalsInMilestone(milestone: MilestoneDocument) { export function CalculateMilestoneOutOf(milestone: MilestoneDocument) { const goalIDs = GetGoalIDsFromMilestone(milestone); - if (milestone.criteria.type === "all") { - return goalIDs.length; - } else if (milestone.criteria.type === "total") { - if (milestone.criteria.value === null) { - throw new Error( - `Invalid milestone ${milestone.milestoneID} - abs and null are not compatible.` - ); + switch (milestone.criteria.type) { + case "all": + return goalIDs.length; + case "total": { + // It's not possible according to the types, but I want to check it anyway. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (milestone.criteria.value === null) { + throw new Error( + `Invalid milestone ${milestone.milestoneID} - total and null are not compatible.` + ); + } + + return milestone.criteria.value; } - return milestone.criteria.value; + default: + // Typescript annoyingly complains that milestone.criteria is of type never now. + // it's right! but if we get here, I want to log the error somehow. + throw new Error( + `Invalid milestone.criteria.type of ${ + (milestone.criteria as MilestoneDocument["criteria"]).type + } -- milestoneID ${milestone.milestoneID}` + ); } - - throw new Error( - // @ts-expect-error Yeah obviously this shouldn't happen. Mayaswell throw though. - `Invalid milestone.criteria.type of ${milestone.criteria.type} -- milestoneID ${milestone.milestoneID}` - ); } type EvaluatedGoalResult = EvaluatedGoalReturn & { goalID: string }; diff --git a/server/src/lib/webhooks/webhooks.ts b/server/src/lib/webhooks/webhooks.ts index d8acaf649..92de57482 100644 --- a/server/src/lib/webhooks/webhooks.ts +++ b/server/src/lib/webhooks/webhooks.ts @@ -33,7 +33,7 @@ export async function EmitWebhookEvent(content: WebhookEvents) { "Content-Type": "application/json", Authorization: `Bearer ${client.clientSecret}`, }, - }).catch((err) => { + }).catch((err: Error) => { // We don't care about errors. It's probably on their end. logger.info(err.message); }); diff --git a/server/src/main.ts b/server/src/main.ts index 72ea9112c..ec50baf33 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -61,7 +61,7 @@ void RunOnInit(); let instance: http.Server | https.Server; -if (ServerConfig.ENABLE_SERVER_HTTPS) { +if (ServerConfig.ENABLE_SERVER_HTTPS === true) { logger.warn( "HTTPS Mode is enabled. This should not be used in production, and you should instead run behind a reverse proxy.", { bootInfo: true } @@ -79,7 +79,7 @@ if (ServerConfig.ENABLE_SERVER_HTTPS) { } process.on("SIGTERM", () => { - HandleSIGTERMGracefully(instance); + void HandleSIGTERMGracefully(instance); }); if (process.env.INVOKE_JOB_RUNNER) { diff --git a/server/src/scripts/clone-and-anonymise-db.ts b/server/src/scripts/clone-and-anonymise-db.ts index f2db74d8f..7eed590b5 100644 --- a/server/src/scripts/clone-and-anonymise-db.ts +++ b/server/src/scripts/clone-and-anonymise-db.ts @@ -7,10 +7,10 @@ const program = new Command(); program .option("--nsFrom ") - .option("--nsTo "); + .option("--nsTo "); program.parse(process.argv); -const options = program.opts(); +const options: { nsTo?: string; nsFrom?: string } = program.opts(); if (!options.nsTo || !options.nsFrom) { throw new Error(`Expected --nsFrom and --nsTo options.`); @@ -103,5 +103,18 @@ function CloneDB(nsFrom: string, nsTo: string) { if (require.main === module) { CloneDB(options.nsFrom, options.nsTo); - AnonymiseDB(options.nsTo); + + // Don't run any risks -- there's no way we're ever accidentally anonymising the production database. + // any nsTo argument MUST start with anon- + if (!options.nsTo.startsWith("anon-")) { + AnonymiseDB(options.nsTo) + .then(() => { + logger.info(`Anonymised database successfully. Saved to ${options.nsTo}.`); + process.exit(0); + }) + .catch((err: unknown) => { + logger.error(`Failed to anonymise database.`, { err }); + process.exit(1); + }); + } } diff --git a/server/src/scripts/email/send-test-email.ts b/server/src/scripts/email/send-test-email.ts index 65c1714a9..11876627f 100644 --- a/server/src/scripts/email/send-test-email.ts +++ b/server/src/scripts/email/send-test-email.ts @@ -10,7 +10,7 @@ program.option("-e, --email "); const logger = CreateLogCtx(__filename); program.parse(process.argv); -const options = program.opts(); +const options: { email?: string } = program.opts(); if (!options.email) { throw new Error(`Need an --email to send to.`); @@ -20,12 +20,17 @@ if (require.main === module) { (async () => { logger.info(`Sending email to ${options.email}.`); await SendEmail( - options.email, + options.email!, "Hello World", MainHTMLWrapper("Hello world! This is a test email for doing things."), "Hello world! This is a test email for doing things." ); logger.info(`Done.`); + process.exit(0); - })(); + })().catch((err: unknown) => { + logger.error(`Failed to send test email.`, { err }); + + process.exit(1); + }); } diff --git a/server/src/scripts/folders/folder-cache.ts b/server/src/scripts/folders/folder-cache.ts index 18c0b476a..33e84f3cb 100644 --- a/server/src/scripts/folders/folder-cache.ts +++ b/server/src/scripts/folders/folder-cache.ts @@ -1,3 +1,11 @@ import { InitaliseFolderChartLookup } from "../../utils/folder"; +import CreateLogCtx from "lib/logger/logger"; -InitaliseFolderChartLookup().then(() => process.exit(0)); +const logger = CreateLogCtx(__filename); + +InitaliseFolderChartLookup() + .then(() => process.exit(0)) + .catch((err: unknown) => { + logger.error(`Failed to initialise folder chart lookup.`, { err }); + process.exit(1); + }); diff --git a/server/src/scripts/iidx-tierlist/get-sp12-data.ts b/server/src/scripts/iidx-tierlist/get-sp12-data.ts index 05c10282c..79874c43d 100644 --- a/server/src/scripts/iidx-tierlist/get-sp12-data.ts +++ b/server/src/scripts/iidx-tierlist/get-sp12-data.ts @@ -65,10 +65,6 @@ async function FetchSP12Data() { continue; } - if (!chart) { - logger.warn(`Couldn't find chart with title ${sh.title}`); - } - for (const key of ["n_clear", "hard", "exh"] as const) { let val; diff --git a/server/src/scripts/init-sequence-docs.ts b/server/src/scripts/init-sequence-docs.ts index 1e8614c97..c251a50af 100644 --- a/server/src/scripts/init-sequence-docs.ts +++ b/server/src/scripts/init-sequence-docs.ts @@ -1,5 +1,13 @@ import { InitSequenceDocs } from "external/mongo/sequence-docs"; +import CreateLogCtx from "lib/logger/logger"; + +const logger = CreateLogCtx(__filename); if (require.main === module) { - InitSequenceDocs().then(() => process.exit(0)); + InitSequenceDocs() + .then(() => process.exit(0)) + .catch((err: unknown) => { + logger.error(`Failed to initialise sequence documents.`, { err }); + process.exit(1); + }); } diff --git a/server/src/scripts/invites/create-invite-code.ts b/server/src/scripts/invites/create-invite-code.ts index a53f7c3ff..0ece90cbd 100644 --- a/server/src/scripts/invites/create-invite-code.ts +++ b/server/src/scripts/invites/create-invite-code.ts @@ -12,13 +12,15 @@ program.option("-c, --code ", "The code for this invite."); program.parse(process.argv); const options = program.opts(); -if (!options.code) { +if (options.code === undefined) { options.code = Random20Hex(); } +const code = options.code as string; + db.invites .insert({ - code: options.code, + code, createdBy: 1, consumed: false, createdAt: Date.now(), @@ -26,6 +28,10 @@ db.invites consumedAt: null, }) .then(() => { - logger.info(`Created invite ${options.code}.`); + logger.info(`Created invite ${code}.`); process.exit(0); + }) + .catch((err: unknown) => { + logger.error(`Failed to create invite ${code}`, { err }); + process.exit(1); }); diff --git a/server/src/scripts/patches/patch-invalid-iidx-grades.ts b/server/src/scripts/patches/patch-invalid-iidx-grades.ts index 1e51cf7c8..014b202ae 100644 --- a/server/src/scripts/patches/patch-invalid-iidx-grades.ts +++ b/server/src/scripts/patches/patch-invalid-iidx-grades.ts @@ -57,5 +57,9 @@ if (require.main === module) { logger.info(`Done!`); process.exit(0); - })(); + })().catch((err: unknown) => { + logger.error(`Failed to patch invalid IIDX grades.`, { err }); + + process.exit(1); + }); } diff --git a/server/src/scripts/set-indexes.ts b/server/src/scripts/set-indexes.ts index f8170880f..1bc414d50 100644 --- a/server/src/scripts/set-indexes.ts +++ b/server/src/scripts/set-indexes.ts @@ -1,16 +1,24 @@ /* eslint-disable no-await-in-loop */ import { SetIndexes } from "../external/mongo/indexes"; import { Command } from "commander"; +import CreateLogCtx from "lib/logger/logger"; const program = new Command(); -program.option("-d, --db ", "The database to index."); +const logger = CreateLogCtx(__filename); + +program.requiredOption("-d, --db ", "The database to index."); program.option( "-r, --reset", "Whether to reset all indexes on this database before indexing or not." ); program.parse(process.argv); -const options = program.opts(); +const options: { db: string; reset?: boolean } = program.opts(); -SetIndexes(options.db, options.reset).then(() => process.exit(0)); +SetIndexes(options.db, options.reset === true) + .then(() => process.exit(0)) + .catch((err: unknown) => { + logger.error(`Failed to set indexes.`, { err }); + process.exit(1); + }); diff --git a/server/src/scripts/state-sync/dedupe-score-ids.ts b/server/src/scripts/state-sync/dedupe-score-ids.ts index 8289b91e8..369a38600 100644 --- a/server/src/scripts/state-sync/dedupe-score-ids.ts +++ b/server/src/scripts/state-sync/dedupe-score-ids.ts @@ -1,26 +1,29 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; +import type { IObjectID } from "monk"; +import type { integer } from "tachi-common"; const logger = CreateLogCtx(__filename); async function DedupeScoreIDs() { - const dups = await db.scores.aggregate( - [ - { - $group: { - _id: "$scoreID", - dups: { $addToSet: "$_id" }, - count: { $sum: 1 }, + const dups: Array<{ id: string; dups: Array; count: integer }> = + await db.scores.aggregate( + [ + { + $group: { + _id: "$scoreID", + dups: { $addToSet: "$_id" }, + count: { $sum: 1 }, + }, }, - }, - { - $match: { - count: { $gt: 1 }, + { + $match: { + count: { $gt: 1 }, + }, }, - }, - ], - { allowDiskUse: true } - ); + ], + { allowDiskUse: true } + ); logger.info(`Found ${dups.length} dups.`); @@ -29,9 +32,14 @@ async function DedupeScoreIDs() { // eslint-disable-next-line no-await-in-loop await db.scores.remove({ _id: { $in: dup.dups } }); } - - logger.info(`Done.`); - process.exit(0); } -DedupeScoreIDs(); +DedupeScoreIDs() + .then(() => { + logger.info(`Finished deduping score IDs.`); + process.exit(0); + }) + .catch((err: unknown) => { + logger.error(`Failed to dedupe score IDs.`, { err }); + process.exit(1); + }); diff --git a/server/src/scripts/state-sync/recalc-all-scores.ts b/server/src/scripts/state-sync/recalc-all-scores.ts index 88723303f..a88e99bb3 100644 --- a/server/src/scripts/state-sync/recalc-all-scores.ts +++ b/server/src/scripts/state-sync/recalc-all-scores.ts @@ -1,5 +1,16 @@ +import CreateLogCtx from "lib/logger/logger"; import { RecalcAllScores } from "utils/calculations/recalc-scores"; +const logger = CreateLogCtx(__filename); + if (require.main === module) { - RecalcAllScores(); + RecalcAllScores() + .then(() => { + logger.info(`Successfully recalced all scores.`); + process.exit(0); + }) + .catch((err: unknown) => { + logger.error(`Failed to recalc all scores.`, { err }); + process.exit(1); + }); } diff --git a/server/src/scripts/state-sync/recalc-game-profiles.ts b/server/src/scripts/state-sync/recalc-game-profiles.ts index 24446b8c4..17caf4690 100644 --- a/server/src/scripts/state-sync/recalc-game-profiles.ts +++ b/server/src/scripts/state-sync/recalc-game-profiles.ts @@ -3,28 +3,30 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/user-game-stats/update-ugs"; import { FormatUserDoc } from "utils/user"; +import type { Game, Playtype, ScoreDocument } from "tachi-common"; const logger = CreateLogCtx(__filename); -export async function CreateGameProfiles() { +export async function RecalcGameProfiles() { const users = await db.users.find({}); for (const user of users) { - const gpts = await db.scores.aggregate([ - { - $match: { - userID: user.id, - }, - }, - { - $group: { - _id: { - game: "$game", - playtype: "$playtype", + const gpts: Array = + await db.scores.aggregate([ + { + $match: { + userID: user.id, }, }, - }, - ]); + { + $group: { + _id: { + game: "$game", + playtype: "$playtype", + }, + }, + }, + ]); logger.info(`Found ${gpts.length} GPTs for ${FormatUserDoc(user)}`); @@ -40,5 +42,10 @@ export async function CreateGameProfiles() { } if (require.main === module) { - CreateGameProfiles().then(() => process.exit(0)); + RecalcGameProfiles() + .then(() => process.exit(0)) + .catch((err: unknown) => { + logger.error(`Failed to recalc game profiles.`, { err }); + process.exit(1); + }); } diff --git a/server/src/scripts/state-sync/recalc-sessions.ts b/server/src/scripts/state-sync/recalc-sessions.ts index fc5c242e4..ab7d9fadf 100644 --- a/server/src/scripts/state-sync/recalc-sessions.ts +++ b/server/src/scripts/state-sync/recalc-sessions.ts @@ -1,5 +1,13 @@ +import CreateLogCtx from "lib/logger/logger"; import { RecalcSessions } from "utils/calculations/recalc-sessions"; +const logger = CreateLogCtx(__filename); + if (require.main === module) { - RecalcSessions().then(() => process.exit(0)); + RecalcSessions() + .then(() => process.exit(0)) + .catch((err: unknown) => { + logger.error(`Failed to recalc sessions.`, { err }); + process.exit(1); + }); } diff --git a/server/src/scripts/state-sync/sync-state.ts b/server/src/scripts/state-sync/sync-state.ts index 320ae3a44..aae60dc73 100644 --- a/server/src/scripts/state-sync/sync-state.ts +++ b/server/src/scripts/state-sync/sync-state.ts @@ -1,4 +1,4 @@ -import { CreateGameProfiles } from "./recalc-game-profiles"; +import { RecalcGameProfiles } from "./recalc-game-profiles"; import CreateLogCtx from "lib/logger/logger"; import { RecalcAllScores, UpdateAllPBs } from "utils/calculations/recalc-scores"; import { RecalcSessions } from "utils/calculations/recalc-sessions"; @@ -8,9 +8,12 @@ const logger = CreateLogCtx(__filename); (async () => { await RecalcAllScores(); await UpdateAllPBs(); - await CreateGameProfiles(); + await RecalcGameProfiles(); await RecalcSessions(); logger.info(`Completely done!`); process.exit(0); -})(); +})().catch((err: unknown) => { + logger.error(`Failed to sync state.`, { err }); + process.exit(1); +}); diff --git a/server/src/scripts/state-sync/update-all-pbs.ts b/server/src/scripts/state-sync/update-all-pbs.ts index 4e28a917a..ae2732abe 100644 --- a/server/src/scripts/state-sync/update-all-pbs.ts +++ b/server/src/scripts/state-sync/update-all-pbs.ts @@ -1,6 +1,17 @@ /* eslint-disable no-await-in-loop */ +import CreateLogCtx from "lib/logger/logger"; import { UpdateAllPBs } from "utils/calculations/recalc-scores"; +const logger = CreateLogCtx(__filename); + if (require.main === module) { - UpdateAllPBs(); + UpdateAllPBs() + .then(() => { + logger.info(`Successfully updated all PBs.`); + process.exit(0); + }) + .catch((err: unknown) => { + logger.error(`Failed to update all PBs.`, { err }); + process.exit(1); + }); } diff --git a/server/src/scripts/sync-database.ts b/server/src/scripts/sync-database.ts index 78999c7fe..49363f2c7 100644 --- a/server/src/scripts/sync-database.ts +++ b/server/src/scripts/sync-database.ts @@ -72,7 +72,7 @@ async function GenericUpsert( const allExistingDocs = await collection.find({}); - const map = new Map(); + const map = new Map(); for (const doc of allExistingDocs) { map.set(doc[field], doc); @@ -90,7 +90,7 @@ async function GenericUpsert( const exists = map.get(document[field]); - if (!exists) { + if (exists === undefined) { bwriteOps.push({ // @ts-expect-error Actually, T is assignable to OptionalId. insertOne: { document }, @@ -227,7 +227,7 @@ const syncInstructions: Array = [ true ); - if (r) { + if (r.thingsChanged) { await InitaliseFolderChartLookup(); const allModifiedFolderIDs = r.changedFields as Array; diff --git a/server/src/scripts/validate-database.ts b/server/src/scripts/validate-database.ts index 3da8f02af..1c6c0d174 100644 --- a/server/src/scripts/validate-database.ts +++ b/server/src/scripts/validate-database.ts @@ -6,6 +6,7 @@ import { DatabaseSchemas } from "external/mongo/schemas"; import CreateLogCtx from "lib/logger/logger"; import { FormatPrError } from "utils/prudence"; import type { Databases } from "external/mongo/db"; +import type { FindResult } from "monk"; import type { PrudenceError } from "prudence"; const program = new Command(); @@ -33,36 +34,48 @@ export async function ValidateCollection(collectionName: Databases): Promise { - try { - schemaRunner(c); - success++; - } catch (err) { - logger.error(`[${collectionName}]: ${FormatPrError(err as PrudenceError)}`, c); - fails++; - } - }); + // Faulty monk types. Returns actually also have a streaming .each operator + // which we leverage here for performance. + await ( + monkDB.get(collectionName).find({}) as Promise> & { + each: (cb: (c: unknown) => void) => Promise; + } + ).each((c) => { + try { + schemaRunner(c); + success++; + } catch (err) { + logger.error(`[${collectionName}]: ${FormatPrError(err as PrudenceError)}`, c); + fails++; + } + }); logger.info(`Success: ${success} (${(100 * success) / documents}%)`); logger.info(`Failures: ${fails} (${(100 * fails) / documents}%)`); } export async function ValidateAllCollections() { - for (const collectionName in DatabaseSchemas) { + for (const collectionName of Object.keys(DatabaseSchemas)) { // eslint-disable-next-line no-await-in-loop await ValidateCollection(collectionName as keyof typeof DatabaseSchemas); } } if (require.main === module) { - if (options.collection) { - ValidateCollection(options.collection).then(() => process.exit(0)); + if (typeof options.collection === "string") { + // @hack This should be typechecked and warned about outside of ValidateCollection. + ValidateCollection(options.collection as Databases) + .then(() => process.exit(0)) + .catch((err: unknown) => { + logger.error(`Failed to validate collection ${options.collection}?`, { err }); + process.exit(1); + }); } else { - ValidateAllCollections().then(() => process.exit(0)); + ValidateAllCollections() + .then(() => process.exit(0)) + .catch((err: unknown) => { + logger.error(`Failed to validate all collections?`, { err }); + process.exit(1); + }); } } diff --git a/server/src/server/middleware/auth.test.ts b/server/src/server/middleware/auth.test.ts index 518a784fa..cef9f0b98 100644 --- a/server/src/server/middleware/auth.test.ts +++ b/server/src/server/middleware/auth.test.ts @@ -1,3 +1,4 @@ +import { SetRequestPermissions } from "./auth"; import expMiddlewareMock from "express-request-mock"; import db from "external/mongo/db"; import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; @@ -5,7 +6,6 @@ import { ALL_PERMISSIONS } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; -import { SetRequestPermissions } from "./auth"; t.test("#SetRequestPermissions", (t) => { t.beforeEach(ResetDBState); @@ -64,6 +64,7 @@ t.test("#SetRequestPermissions", (t) => { t.equal(res.statusCode, 400); const json = res._getJSONData(); + t.match(json.description, /Invalid Authorization Type - Expected Bearer/u); t.end(); }); @@ -78,6 +79,7 @@ t.test("#SetRequestPermissions", (t) => { t.equal(res.statusCode, 401); const json = res._getJSONData(); + t.match(json.description, /Invalid token/u); t.end(); }); @@ -92,6 +94,7 @@ t.test("#SetRequestPermissions", (t) => { t.equal(res.statusCode, 401); const json = res._getJSONData(); + t.match( json.description, /The provided API token does not correspond with any key in the database/u diff --git a/server/src/server/middleware/auth.ts b/server/src/server/middleware/auth.ts index 69fa958cb..fc9967f34 100644 --- a/server/src/server/middleware/auth.ts +++ b/server/src/server/middleware/auth.ts @@ -119,7 +119,9 @@ export const SetFervidexStyleRequestPermissions: RequestHandler = export const RequirePermissions = (...perms: Array): RequestHandler => (req, res, next) => { - if (!req[SYMBOL_TACHI_API_AUTH]) { + // This isn't possible on paper, but maybe some insane stuff has happened to lead this to happen. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (req[SYMBOL_TACHI_API_AUTH] === undefined) { logger.error( `RequirePermissions middleware was hit without any TachiAPIAuthentication?` ); @@ -167,7 +169,9 @@ export const RequirePermissions = const CreateRequireNotGuest = (errorKeyName: string): RequestHandler => (req, res, next) => { - if (!req[SYMBOL_TACHI_API_AUTH]) { + // See above -- this isn't possible on paper, but I want to check it anyway. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (req[SYMBOL_TACHI_API_AUTH] === undefined) { logger.error(`RequirePermissions middleware was hit without any TachiAPIData?`); return res.status(500).json({ success: false, diff --git a/server/src/server/middleware/multer-upload.ts b/server/src/server/middleware/multer-upload.ts index 297ac8a78..36bdd56f4 100644 --- a/server/src/server/middleware/multer-upload.ts +++ b/server/src/server/middleware/multer-upload.ts @@ -29,7 +29,7 @@ export const CreateMulterSingleUploadMiddleware = ( description: "File provided was too large, corrupt, or provided in the wrong field.", }); - } else if (err) { + } else if (err !== undefined && err !== null) { logger.error(`Unknown file import error: ${err}`, { err }); return res.status(500).json({ diff --git a/server/src/server/middleware/prudence-validate.test.ts b/server/src/server/middleware/prudence-validate.test.ts index f7bec380b..df3d5b758 100644 --- a/server/src/server/middleware/prudence-validate.test.ts +++ b/server/src/server/middleware/prudence-validate.test.ts @@ -1,7 +1,7 @@ +import prValidate from "./prudence-validate"; import expMiddlewareMock from "express-request-mock"; import Prudence from "prudence"; import t from "tap"; -import prValidate from "./prudence-validate"; t.test("#PrudenceMiddleware", (t) => { const mw = prValidate({ foo: Prudence.regex(/^baz$/u) }, { foo: "example error message" }); @@ -16,6 +16,7 @@ t.test("#PrudenceMiddleware", (t) => { t.equal(res.statusCode, 400, "Status code should be 400"); const json = res._getJSONData(); + t.equal( json.description, "[foo] example error message (Received bar)", @@ -33,6 +34,7 @@ t.test("#PrudenceMiddleware", (t) => { t.equal(res.statusCode, 400, "Status code should be 400"); const json = res._getJSONData(); + t.equal( json.description, "[foo] example error message (Received nothing)", @@ -84,6 +86,7 @@ t.test("#PrudenceMiddleware", (t) => { t.equal(res.statusCode, 400, "Status code should be 400"); const json = res._getJSONData(); + t.equal( json.description, "[foo] example error message (Received bar)", @@ -110,6 +113,7 @@ t.test("#PrudenceMiddleware", (t) => { t.equal(res.statusCode, 400, "Status code should be 400"); const json = res._getJSONData(); + t.equal( json.description, "[!password] invalid password (Received ****)", @@ -132,6 +136,7 @@ t.test("#PrudenceMiddleware", (t) => { t.equal(res.statusCode, 400, "Status code should be 400"); const json = res._getJSONData(); + t.equal( json.description, "[!password] invalid password (Received nothing)", diff --git a/server/src/server/middleware/prudence-validate.ts b/server/src/server/middleware/prudence-validate.ts index 688b659ec..6d9f1fb58 100644 --- a/server/src/server/middleware/prudence-validate.ts +++ b/server/src/server/middleware/prudence-validate.ts @@ -18,15 +18,15 @@ const API_ERR_HANDLER = (req, res, next, error) => { let stringVal = error.userVal; - if (error.keychain && error.keychain.includes("password") && error.userVal) { + if (error.keychain?.startsWith("!") === true) { stringVal = "****"; } - if (typeof stringVal === "object" && stringVal !== null && !stringVal.toString) { + if (typeof stringVal === "object" && stringVal !== null) { // this is probably null-prototype stringVal = null; } else if (stringVal === undefined) { - stringVal = "nothing"; + stringVal = "nothing (undefined)"; } else { stringVal = String(stringVal); } diff --git a/server/src/server/middleware/rate-limiter.ts b/server/src/server/middleware/rate-limiter.ts index a8ff3597d..3357165ec 100644 --- a/server/src/server/middleware/rate-limiter.ts +++ b/server/src/server/middleware/rate-limiter.ts @@ -12,9 +12,11 @@ import type { integer } from "tachi-common"; const logger = CreateLogCtx(__filename); function CreateStore(name: string) { + // undefined forces a default to an in-memory store + // So we use that when in testing or localdev. return Environment.nodeEnv === "production" || Environment.nodeEnv === "staging" ? new RateLimitRedis({ prefix: `${TachiConfig.NAME}-RL:${name}`, client: RedisClient }) - : undefined; // undefined forces a default to an in-memory store + : undefined; } export function ClearTestingRateLimitCache() { @@ -63,8 +65,8 @@ export const HyperAggressiveRateLimitMiddleware = rateLimit( // 5 requests every minute. This one has a tighter window, so it is less // vulnerable to bursting down the server. +// if we're in testing, disable this rate limit! export const ScoreImportRateLimiter = - // if we're in testing, disable this rate limit! Environment.nodeEnv === "test" ? rateLimit(CreateRateLimitOptions(Infinity, "ScImport", ONE_MINUTE)) : rateLimit(CreateRateLimitOptions(5, "ScImport", ONE_MINUTE)); diff --git a/server/src/server/middleware/request-logger.ts b/server/src/server/middleware/request-logger.ts index bf09023db..2f43d7d7a 100644 --- a/server/src/server/middleware/request-logger.ts +++ b/server/src/server/middleware/request-logger.ts @@ -40,6 +40,7 @@ export const RequestLoggerMiddleware: RequestHandler = (req, res, next) => { res.on("finish", () => { const contents = { // @ts-expect-error we're doing some monkey patching - contentBody is what we're returning. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment body: res.contentBody, statusCode: res.statusCode, requestQuery: req.query, @@ -54,6 +55,8 @@ export const RequestLoggerMiddleware: RequestHandler = (req, res, next) => { return; } + // 403 bannings like this are also spam. + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (contents.body?.description === `You are banned from ${TachiConfig.NAME}.`) { return; } diff --git a/server/src/server/middleware/sanitise-qstring.test.ts b/server/src/server/middleware/sanitise-qstring.test.ts index efab76a62..ecbee634d 100644 --- a/server/src/server/middleware/sanitise-qstring.test.ts +++ b/server/src/server/middleware/sanitise-qstring.test.ts @@ -1,6 +1,6 @@ +import SanitiseQString from "./sanitise-qstring"; import expMiddlewareMock from "express-request-mock"; import t from "tap"; -import SanitiseQString from "./sanitise-qstring"; t.test("#SanitiseQString", (t) => { t.test("Should allow GET requests with valid data.", async (t) => { diff --git a/server/src/server/middleware/sanitise-qstring.ts b/server/src/server/middleware/sanitise-qstring.ts index ce78ce57f..18e76f996 100644 --- a/server/src/server/middleware/sanitise-qstring.ts +++ b/server/src/server/middleware/sanitise-qstring.ts @@ -5,11 +5,11 @@ import type { NextFunction, Request, Response } from "express"; */ function SanitiseQString(req: Request, res: Response, next: NextFunction) { if (req.method === "GET") { - for (const data in req.query) { - if (typeof req.query[data] !== "string") { + for (const [key, value] of Object.entries(req.query)) { + if (typeof value !== "string") { return res.status(400).json({ success: false, - description: `Invalid request with key ${data}`, + description: `Invalid request with key ${key}`, }); } } diff --git a/server/src/server/middleware/update-last-seen.ts b/server/src/server/middleware/update-last-seen.ts index b9395860f..7cb651c33 100644 --- a/server/src/server/middleware/update-last-seen.ts +++ b/server/src/server/middleware/update-last-seen.ts @@ -2,7 +2,7 @@ import db from "external/mongo/db"; import type { RequestHandler } from "express"; export const UpdateLastSeen: RequestHandler = (req, res, next) => { - if (!req.session.tachi?.user.id) { + if (req.session.tachi?.user.id === undefined) { next(); return; } @@ -13,7 +13,7 @@ export const UpdateLastSeen: RequestHandler = (req, res, next) => { } // fire, but we have no reason to await it. - db.users.update( + void db.users.update( { id: req.session.tachi.user.id }, { $set: { diff --git a/server/src/server/router/api/v1/admin/router.test.ts b/server/src/server/router/api/v1/admin/router.test.ts index 89f6cb6f2..f4c995913 100644 --- a/server/src/server/router/api/v1/admin/router.test.ts +++ b/server/src/server/router/api/v1/admin/router.test.ts @@ -3,12 +3,13 @@ import db from "external/mongo/db"; import { ONE_MINUTE } from "lib/constants/time"; import { ChangeRootLogLevel, GetLogLevel } from "lib/logger/logger"; import { ServerConfig } from "lib/setup/config"; -import { ScoreDocument } 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 { TestingIIDXSPScore } from "test-utils/test-data"; +import { Sleep } from "utils/misc"; +import type { ScoreDocument } from "tachi-common"; const LOG_LEVEL = ServerConfig.LOGGER_CONFIG.LOG_LEVEL; @@ -53,7 +54,7 @@ t.test("POST /api/v1/admin/change-log-level", async (t) => { t.equal(GetLogLevel(), "warn"); // wait a bit - await new Promise((resolve) => setTimeout(() => resolve(), ONE_MINUTE * 0.06)); + await Sleep(ONE_MINUTE * 0.06); t.equal(GetLogLevel(), LOG_LEVEL); diff --git a/server/src/server/router/api/v1/auth/auth.test.ts b/server/src/server/router/api/v1/auth/auth.test.ts index ecc0b2c80..12ff7100e 100644 --- a/server/src/server/router/api/v1/auth/auth.test.ts +++ b/server/src/server/router/api/v1/auth/auth.test.ts @@ -1,8 +1,8 @@ +import { AddNewInvite, ReinstateInvite, ValidateCaptcha } from "./auth"; import db from "external/mongo/db"; import t from "tap"; import { MockJSONFetch } from "test-utils/mock-fetch"; import ResetDBState from "test-utils/resets"; -import { AddNewInvite, ReinstateInvite, ValidateCaptcha } from "./auth"; t.test("#ReinstateInvite", (t) => { t.beforeEach(ResetDBState); @@ -23,7 +23,8 @@ t.test("#ReinstateInvite", (t) => { t.equal(response.nModified, 1, "Should modify one document"); const invite2 = await db.invites.findOne({ - code: inviteDoc.code, // lol + // lol + code: inviteDoc.code, }); t.equal(invite2!.consumed, false, "Should no longer be consumed"); diff --git a/server/src/server/router/api/v1/auth/auth.ts b/server/src/server/router/api/v1/auth/auth.ts index 36c9caa2e..7983548cf 100644 --- a/server/src/server/router/api/v1/auth/auth.ts +++ b/server/src/server/router/api/v1/auth/auth.ts @@ -57,19 +57,6 @@ export async function AddNewInvite(user: PublicUserDocument) { logger.info(`User ${FormatUserDoc(user)} created an invite.`); - if (!result) { - logger.error( - `Fatal error in creating ${FormatUserDoc( - user - )}'s invite code. Database refused key ${code}.` - ); - throw new Error( - `Fatal error in creating ${FormatUserDoc( - user - )}'s invite code. Database refused key ${code}.` - ); - } - return result; } diff --git a/server/src/server/router/api/v1/auth/router.test.ts b/server/src/server/router/api/v1/auth/router.test.ts index d6928acdd..e439fdcfa 100644 --- a/server/src/server/router/api/v1/auth/router.test.ts +++ b/server/src/server/router/api/v1/auth/router.test.ts @@ -1,11 +1,10 @@ -import t from "tap"; +import { PasswordCompare } from "./auth"; import db from "external/mongo/db"; - +import { ClearTestingRateLimitCache } from "server/middleware/rate-limiter"; +import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { Sleep } from "utils/misc"; -import { PasswordCompare } from "./auth"; -import { ClearTestingRateLimitCache } from "server/middleware/rate-limiter"; t.test("POST /api/v1/auth/login", (t) => { t.beforeEach(ResetDBState); @@ -186,7 +185,9 @@ t.test("POST /api/v1/auth/register", (t) => { const res = await mockApi.post("/api/v1/auth/register").send({ username: "foo", "!password": "password", - email: "thepasswordis@password.com", // this is our test docs email, apparently. + + // this is our test docs email, apparently. + email: "thepasswordis@password.com", captcha: "1", inviteCode: "code", }); @@ -254,7 +255,8 @@ t.test("POST /api/v1/auth/register", (t) => { }); t.test("Should recover from a fatal error without breaking state.", async (t) => { - await db.counters.update({ counterName: "users" }, { $set: { value: 1 } }); // this will cause a userID collision + // this will cause a userID collision + await db.counters.update({ counterName: "users" }, { $set: { value: 1 } }); const res = await mockApi.post("/api/v1/auth/register").send({ username: "foo", diff --git a/server/src/server/router/api/v1/clients/middleware.test.ts b/server/src/server/router/api/v1/clients/middleware.test.ts index 6a6ee478b..dcc5e99db 100644 --- a/server/src/server/router/api/v1/clients/middleware.test.ts +++ b/server/src/server/router/api/v1/clients/middleware.test.ts @@ -1,8 +1,8 @@ +import { GetClientFromID, RequireOwnershipOfClient } from "./middleware"; import expMiddlewareMock from "express-request-mock"; import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import t from "tap"; import ResetDBState from "test-utils/resets"; -import { GetClientFromID, RequireOwnershipOfClient } from "./middleware"; t.test("#GetClientFromID", (t) => { t.beforeEach(ResetDBState); @@ -12,13 +12,14 @@ t.test("#GetClientFromID", (t) => { params: { clientID: "OAUTH2_CLIENT_ID", }, - [SYMBOL_TACHI_DATA]: {} + [SYMBOL_TACHI_DATA]: {}, }); t.strictSame( req[SYMBOL_TACHI_DATA]?.apiClientDoc, { clientID: "OAUTH2_CLIENT_ID", + // clientSecret: "OAUTH2_CLIENT_SECRET", name: "Test_Service", author: 1, @@ -39,7 +40,7 @@ t.test("#GetClientFromID", (t) => { params: { clientID: "NONSENSE", }, - [SYMBOL_TACHI_DATA]: {} + [SYMBOL_TACHI_DATA]: {}, }); t.equal(res.statusCode, 404); diff --git a/server/src/server/router/api/v1/clients/router.test.ts b/server/src/server/router/api/v1/clients/router.test.ts index b7590b742..503bad09b 100644 --- a/server/src/server/router/api/v1/clients/router.test.ts +++ b/server/src/server/router/api/v1/clients/router.test.ts @@ -1,12 +1,12 @@ import db from "external/mongo/db"; import { ServerConfig } from "lib/setup/config"; -import { APITokenDocument, TachiAPIClientDocument } 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 type { APITokenDocument, TachiAPIClientDocument } from "tachi-common"; -const clientDataset: TachiAPIClientDocument[] = [ +const clientDataset: Array = [ { author: 1, clientID: "CLIENT_1", @@ -253,6 +253,7 @@ t.test("GET /api/v1/clients/:clientID", (t) => { t.strictSame(res.body.body, { clientID: "OAUTH2_CLIENT_ID", + // clientSecret: "OAUTH2_CLIENT_SECRET", MUST NOT have secret! name: "Test_Service", author: 1, @@ -433,7 +434,7 @@ t.test("DELETE /api/v1/clients/:clientID", async (t) => { token: "bar", userID: 1, }, - ] as APITokenDocument[]); + ] as Array); const res = await mockApi.delete("/api/v1/clients/CLIENT_1").set("Cookie", cookie); diff --git a/server/src/server/router/api/v1/games/_game/_playtype/charts/_chartID/middleware.ts b/server/src/server/router/api/v1/games/_game/_playtype/charts/_chartID/middleware.ts index 6ece357c8..82af0057e 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/charts/_chartID/middleware.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/charts/_chartID/middleware.ts @@ -7,9 +7,11 @@ export const ValidateAndGetChart: RequestHandler = async (req, res, next) => { const chart = await db.charts[game].findOne({ chartID: req.params.chartID, - playtype, // technically redundant, but we're under playtypes here URL wise. + + // technically redundant, but we're under playtypes here URL wise. // this means we cant match an SP chart when we're under IIDX SP, for // example. + playtype, }); if (!chart) { diff --git a/server/src/server/router/api/v1/games/_game/_playtype/charts/_chartID/router.test.ts b/server/src/server/router/api/v1/games/_game/_playtype/charts/_chartID/router.test.ts index 01a278769..d332cddc9 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/charts/_chartID/router.test.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/charts/_chartID/router.test.ts @@ -1,10 +1,10 @@ import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import { PBScoreDocument, PublicUserDocument } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { Testing511SPA, TestingIIDXSPScorePB } from "test-utils/test-data"; +import type { PBScoreDocument, PublicUserDocument } from "tachi-common"; t.test("GET /api/v1/games/:game/:playtype/charts/:chartID", (t) => { t.beforeEach(ResetDBState); @@ -76,7 +76,7 @@ t.test("GET /api/v1/games/:game/:playtype/charts/:chartID/playcount", (t) => { t.beforeEach(ResetDBState); t.test("Should return the total playcount on this chart.", async (t) => { - const pbs: PBScoreDocument[] = []; + const pbs: Array = []; delete TestingIIDXSPScorePB._id; diff --git a/server/src/server/router/api/v1/games/_game/_playtype/charts/_chartID/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/charts/_chartID/router.ts index 2e3bb09a9..7cc0f2ad3 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/charts/_chartID/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/charts/_chartID/router.ts @@ -76,7 +76,7 @@ router.get("/folders", async (req, res) => { folderID: { $in: folderIDs.map((e) => e.folderID) }, }; - if (!req.query.inactive) { + if (req.query.inactive === undefined) { query.inactive = false; } diff --git a/server/src/server/router/api/v1/games/_game/_playtype/charts/router.test.ts b/server/src/server/router/api/v1/games/_game/_playtype/charts/router.test.ts index 138515154..1fe1e0715 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/charts/router.test.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/charts/router.test.ts @@ -1,9 +1,9 @@ import db from "external/mongo/db"; -import { PBScoreDocument } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { LoadTachiIIDXData, Testing511SPA } from "test-utils/test-data"; +import type { PBScoreDocument } from "tachi-common"; t.test("GET /api/v1/games/:game/:playtype/charts", (t) => { t.beforeEach(ResetDBState); @@ -24,10 +24,11 @@ t.test("GET /api/v1/games/:game/:playtype/charts", (t) => { userID: 3, }, { - chartID: "fc7edc6bcfa701a261c89c999ddbba3e2195597b", //gambol hyper + // gambol hyper + chartID: "fc7edc6bcfa701a261c89c999ddbba3e2195597b", userID: 1, }, - ] as PBScoreDocument[]); + ] as Array); const res = await mockApi.get("/api/v1/games/iidx/SP/charts"); @@ -49,10 +50,11 @@ t.test("GET /api/v1/games/:game/:playtype/charts", (t) => { t.test("Should search charts if a search param is set.", async (t) => { await db["personal-bests"].insert([ { - chartID: "fc7edc6bcfa701a261c89c999ddbba3e2195597b", //gambol hyper + // gambol hyper + chartID: "fc7edc6bcfa701a261c89c999ddbba3e2195597b", userID: 1, }, - ] as PBScoreDocument[]); + ] as Array); const res = await mockApi.get("/api/v1/games/iidx/SP/charts?search=gambol"); diff --git a/server/src/server/router/api/v1/games/_game/_playtype/charts/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/charts/router.ts index e08b29be0..ce00aad5b 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/charts/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/charts/router.ts @@ -57,8 +57,8 @@ router.get("/", async (req, res) => { // // Since most players will have this off, this is not a significant // performance hit. - if (game === "iidx" && !req.query.noIntelligentOmit) { - if (!req[SYMBOL_TACHI_API_AUTH].userID) { + if (game === "iidx" && req.query.noIntelligentOmit === undefined) { + if (req[SYMBOL_TACHI_API_AUTH].userID === null) { charts = charts.filter( (e) => (e as ChartDocument<"iidx:DP" | "iidx:SP">).data["2dxtraSet"] === null ); diff --git a/server/src/server/router/api/v1/games/_game/_playtype/folders/router.test.ts b/server/src/server/router/api/v1/games/_game/_playtype/folders/router.test.ts index a1f11efde..cb817b7f9 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/folders/router.test.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/folders/router.test.ts @@ -1,11 +1,11 @@ import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import { FolderDocument } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { Testing511SPA } from "test-utils/test-data"; import { CreateFolderChartLookup } from "utils/folder"; +import type { FolderDocument } from "tachi-common"; const mockFolder: FolderDocument = { folderID: "foo", diff --git a/server/src/server/router/api/v1/games/_game/_playtype/folders/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/folders/router.ts index 879853adf..00d7035c9 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/folders/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/folders/router.ts @@ -26,10 +26,15 @@ router.get("/", async (req, res) => { }); } + // if inactive is passed, we need this to be undefined so that + // mongodb returns both inactive and active folders. + // Otherwise, only return active folders. + const inactive = req.query.inactive === undefined ? false : undefined; + const folders = await SearchCollection( db.folders, req.query.search, - { game, playtype, inactive: !!req.query.inactive }, + { game, playtype, inactive }, 100 ); diff --git a/server/src/server/router/api/v1/games/_game/_playtype/router.test.ts b/server/src/server/router/api/v1/games/_game/_playtype/router.test.ts index fc9258d16..854cbbf16 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/router.test.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/router.test.ts @@ -1,10 +1,11 @@ import dm from "deepmerge"; import db from "external/mongo/db"; -import { GetGamePTConfig, PublicUserDocument, UserGameStats } from "tachi-common"; +import { GetGamePTConfig } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { FakeOtherUser } from "test-utils/test-data"; +import type { PublicUserDocument, UserGameStats } from "tachi-common"; t.test("GET /api/v1/games/:game/:playtype", (t) => { t.test("Should return information about the game:playtype.", async (t) => { @@ -85,7 +86,7 @@ t.test("GET /api/v1/games/:game/:playtype/leaderboard", (t) => { BPI: 50, }, }, - ] as UserGameStats[]); + ] as Array); await db.users.insert([ FakeOtherUser, diff --git a/server/src/server/router/api/v1/games/_game/_playtype/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/router.ts index f3bd6b64c..2685ce52c 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/router.ts @@ -23,7 +23,7 @@ import { } from "utils/string-checks"; import { GetUsersWithIDs } from "utils/user"; import type { FindOptions } from "monk"; -import type { Game, integer, Playtype, UserGameStats } from "tachi-common"; +import type { Game, integer, Playtype, UserGameStats, gameClasses, IDStrings } from "tachi-common"; const router: Router = Router({ mergeParams: true }); @@ -37,7 +37,7 @@ async function GetGameStats( ): Promise<{ scoreCount: integer; playerCount: integer; chartCount: integer }> { const cacheRes = gptStatCache.get(`${game}:${playtype}`); - if (!cacheRes) { + if (cacheRes === undefined) { const [scoreCount, playerCount, chartCount] = await Promise.all([ db.scores.count({ game, @@ -242,10 +242,11 @@ router.get( }); } - // @hack This is asserted above. We're just going to cast as any because we know what - // we're doing. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const distribution = await GetClassDistribution(game, playtype, stat as any); + const distribution = await GetClassDistribution( + game, + playtype, + stat as gameClasses.GameClassSets[IDStrings] + ); return res.status(200).json({ success: true, @@ -270,7 +271,8 @@ router.get( async (req, res) => { const { game, playtype } = GetGPT(req); - const limit = req.query.limit ? Number(req.query.limit) : 10; + // validated to never be NaN by prudence. + const limit = req.query.limit !== undefined ? Number(req.query.limit) : 10; const recentClasses = await db["class-achievements"].find( { diff --git a/server/src/server/router/api/v1/games/_game/_playtype/scores/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/scores/router.ts index 7852f62fe..40c8aa3c1 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/scores/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/scores/router.ts @@ -23,7 +23,8 @@ router.get( async (req, res) => { const { game, playtype } = GetGPT(req); - const limit = req.query.limit ? Number(req.query.limit) : 100; + // validated to not be NaN by prudence. + const limit = req.query.limit !== undefined ? Number(req.query.limit) : 100; const scores = await db.scores.find( { diff --git a/server/src/server/router/api/v1/games/_game/_playtype/tables/router.test.ts b/server/src/server/router/api/v1/games/_game/_playtype/tables/router.test.ts index 9fa5c9cd0..5de956b39 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/tables/router.test.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/tables/router.test.ts @@ -1,11 +1,10 @@ import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import { FolderDocument } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { TestingIIDXFolderSP10 } from "test-utils/test-data"; - +import type { FolderDocument } from "tachi-common"; t.test("GET /api/v1/games/:game/:playtype/tables", (t) => { t.beforeEach(ResetDBState); diff --git a/server/src/server/router/api/v1/games/_game/_playtype/tables/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/tables/router.ts index 8767a1331..ed9bffc39 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/tables/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/tables/router.ts @@ -20,7 +20,7 @@ router.get("/", async (req, res) => { const query: FilterQuery = { game, playtype }; - if (!req.query.showInactive) { + if (req.query.showInactive === undefined) { query.inactive = false; } diff --git a/server/src/server/router/api/v1/games/_game/_playtype/targets/goals/router.test.ts b/server/src/server/router/api/v1/games/_game/_playtype/targets/goals/router.test.ts index a93120920..92efdd40e 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/targets/goals/router.test.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/targets/goals/router.test.ts @@ -1,15 +1,15 @@ import dm from "deepmerge"; import db from "external/mongo/db"; -import { GoalDocument, GoalSubscriptionDocument } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { - FakeOtherUser, - IIDXSPMilestoneGoals, - IIDXSPMilestoneGoalSubs, - TestingIIDXSPMilestone + FakeOtherUser, + IIDXSPMilestoneGoals, + IIDXSPMilestoneGoalSubs, + TestingIIDXSPMilestone, } from "test-utils/test-data"; +import type { GoalDocument, GoalSubscriptionDocument } from "tachi-common"; // this is my lazy sample data for these tests. const LoadLazySampleData = async () => { @@ -20,7 +20,7 @@ const LoadLazySampleData = async () => { dm(IIDXSPMilestoneGoalSubs[0]!, { userID: 2, }), - ] as GoalSubscriptionDocument[]); + ] as Array); }; t.test("GET /api/v1/games/:game/:playtype/targets/goals/popular", (t) => { @@ -39,7 +39,7 @@ t.test("GET /api/v1/games/:game/:playtype/targets/goals/popular", (t) => { // note: we have to sort the output here such that it's deterministic. t.strictSame( - (res.body.body as GoalDocument[]).sort((a, b) => a.goalID.localeCompare(b.goalID)), + (res.body.body as Array).sort((a, b) => a.goalID.localeCompare(b.goalID)), ( [ // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -50,7 +50,7 @@ t.test("GET /api/v1/games/:game/:playtype/targets/goals/popular", (t) => { dm(IIDXSPMilestoneGoals[2] as any, { __subscriptions: 1 }), // eslint-disable-next-line @typescript-eslint/no-explicit-any dm(IIDXSPMilestoneGoals[3] as any, { __subscriptions: 1 }), - ] as unknown as GoalDocument[] + ] as unknown as Array ).sort((a, b) => a.goalID.localeCompare(b.goalID)), "Should return the most subscribed goals." ); diff --git a/server/src/server/router/api/v1/games/_game/_playtype/targets/milestone-sets/router.test.ts b/server/src/server/router/api/v1/games/_game/_playtype/targets/milestone-sets/router.test.ts index bc1086fc9..5998f28d7 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/targets/milestone-sets/router.test.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/targets/milestone-sets/router.test.ts @@ -1,10 +1,10 @@ import dm from "deepmerge"; import db from "external/mongo/db"; -import { MilestoneDocument, MilestoneSetDocument } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { TestingIIDXSPMilestone } from "test-utils/test-data"; +import type { MilestoneDocument, MilestoneSetDocument } from "tachi-common"; const TestingIIDXSPMilestoneSet: MilestoneSetDocument = { name: "Testing Milestone Set", @@ -40,7 +40,9 @@ t.test("GET /api/v1/games/:game/:playtype/targets/milestone-sets", (t) => { ); t.hasStrict( - (res.body.body as MilestoneSetDocument[]).sort((a, b) => a.name.localeCompare(b.name)), + (res.body.body as Array).sort((a, b) => + a.name.localeCompare(b.name) + ), [{ setID: "similar_name" }, { setID: "name" }] ); @@ -78,7 +80,7 @@ t.test("GET /api/v1/games/:game/:playtype/targets/milestone-sets/:setID", (t) => }); t.hasStrict( - (res.body.body.milestones as MilestoneDocument[]).sort((a, b) => + (res.body.body.milestones as Array).sort((a, b) => a.milestoneID.localeCompare(b.milestoneID) ), [ diff --git a/server/src/server/router/api/v1/games/_game/_playtype/targets/milestones/router.test.ts b/server/src/server/router/api/v1/games/_game/_playtype/targets/milestones/router.test.ts index c4f1fb735..151afab72 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/targets/milestones/router.test.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/targets/milestones/router.test.ts @@ -1,21 +1,21 @@ import dm from "deepmerge"; import db from "external/mongo/db"; -import { - MilestoneDocument, - MilestoneSetDocument, - MilestoneSubscriptionDocument -} from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { - FakeOtherUser, - IIDXSPMilestoneGoals, - IIDXSPMilestoneGoalSubs, - TestingIIDXSPMilestone, - TestingIIDXSPMilestoneSub, - TestingIIDXSPScorePB + FakeOtherUser, + IIDXSPMilestoneGoals, + IIDXSPMilestoneGoalSubs, + TestingIIDXSPMilestone, + TestingIIDXSPMilestoneSub, + TestingIIDXSPScorePB, } from "test-utils/test-data"; +import type { + MilestoneDocument, + MilestoneSetDocument, + MilestoneSubscriptionDocument, +} from "tachi-common"; t.test("GET /api/v1/games/:game/:playtype/targets/milestones", (t) => { t.beforeEach(ResetDBState); @@ -42,7 +42,9 @@ t.test("GET /api/v1/games/:game/:playtype/targets/milestones", (t) => { t.equal(res.statusCode, 200); t.hasStrict( - (res.body.body as MilestoneDocument[]).sort((a, b) => a.name.localeCompare(b.name)), + (res.body.body as Array).sort((a, b) => + a.name.localeCompare(b.name) + ), [{ milestoneID: "similar_name" }, { milestoneID: "name" }] ); @@ -66,7 +68,7 @@ const LoadLazySampleData = async () => { dm(TestingIIDXSPMilestoneSub, { userID: 2, }), - ] as MilestoneSubscriptionDocument[]); + ] as Array); }; t.test("GET /api/v1/games/:game/:playtype/targets/milestones/popular", (t) => { @@ -79,7 +81,9 @@ t.test("GET /api/v1/games/:game/:playtype/targets/milestones/popular", (t) => { t.equal(res.statusCode, 200); t.hasStrict( - (res.body.body as MilestoneDocument[]).sort((a, b) => a.name.localeCompare(b.name)), + (res.body.body as Array).sort((a, b) => + a.name.localeCompare(b.name) + ), [ { milestoneID: TestingIIDXSPMilestone.milestoneID, __subscriptions: 2 }, { milestoneID: "other_milestone", __subscriptions: 1 }, diff --git a/server/src/server/router/api/v1/games/_game/_playtype/targets/router.test.ts b/server/src/server/router/api/v1/games/_game/_playtype/targets/router.test.ts index 3acf71415..4100115dc 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/targets/router.test.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/targets/router.test.ts @@ -1,11 +1,10 @@ -import dm from "deepmerge"; import db from "external/mongo/db"; -import { GoalSubscriptionDocument } from "tachi-common"; import t from "tap"; import { mkFakeGoal, mkFakeGoalSub } from "test-utils/misc"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { HC511UserGoal } from "test-utils/test-data"; +import type { GoalSubscriptionDocument } from "tachi-common"; t.test("GET /api/v1/games/:game/:playtype/targets/recently-achieved", (t) => { t.beforeEach(ResetDBState); @@ -15,7 +14,7 @@ t.test("GET /api/v1/games/:game/:playtype/targets/recently-achieved", (t) => { mkFakeGoal({ goalID: "achieved" }), mkFakeGoal({ goalID: "achieved_more_recently" }), mkFakeGoal({ goalID: "achieved_instantly" }), - ]) + ]); await db["goal-subs"].insert([ // not achieved @@ -57,13 +56,19 @@ t.test("GET /api/v1/games/:game/:playtype/targets/recently-raised", (t) => { mkFakeGoal({ goalID: "achieved" }), mkFakeGoal({ goalID: "achieved_instantly" }), ]); - + await db["goal-subs"].insert([ // not achieved HC511UserGoal, mkFakeGoalSub({ goalID: "interacted", achieved: false, lastInteraction: 1000 }), + // happened more recently - mkFakeGoalSub({ goalID: "interacted_more_recently", achieved: false, lastInteraction: 2000 }), + mkFakeGoalSub({ + goalID: "interacted_more_recently", + achieved: false, + lastInteraction: 2000, + }), + // shouldnt be included -- just recently-raised. mkFakeGoalSub({ goalID: "achieved", diff --git a/server/src/server/router/api/v1/import/router.ts b/server/src/server/router/api/v1/import/router.ts index ca45e5baf..c0e276614 100644 --- a/server/src/server/router/api/v1/import/router.ts +++ b/server/src/server/router/api/v1/import/router.ts @@ -132,7 +132,7 @@ router.post( }; // Fire the score import, but make no guarantees about its state. - MakeScoreImport(job); + void MakeScoreImport(job); return res.status(202).json({ success: true, diff --git a/server/src/server/router/api/v1/imports/router.test.ts b/server/src/server/router/api/v1/imports/router.test.ts index 8e5bf3081..c606e4bb4 100644 --- a/server/src/server/router/api/v1/imports/router.test.ts +++ b/server/src/server/router/api/v1/imports/router.test.ts @@ -75,6 +75,7 @@ t.test("POST /api/v1/imports/:importID/revert", async (t) => { t.resolveMatch( db.scores.findOne({ scoreID: FakeImport.scoreIDs[0] }), + // @ts-expect-error https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60020 null, "The scores that were part of this import should be deleted." @@ -85,6 +86,7 @@ t.test("POST /api/v1/imports/:importID/revert", async (t) => { t.test("Should return 404 if the import doesn't exist.", async (t) => { const res = await mockApi.post(`/api/v1/imports/doesnt-exist/revert`).set("Cookie", cookie); + t.equal(res.statusCode, 404, "Should return 404."); t.end(); @@ -109,6 +111,7 @@ t.test("POST /api/v1/imports/:importID/revert", async (t) => { ); const someoneElsesImport = mkFakeImport({ userID: 2, importID: "someone_elses" }); + await db.imports.insert(someoneElsesImport); const res = await mockApi @@ -133,6 +136,7 @@ t.test("POST /api/v1/imports/:importID/revert", async (t) => { ); const someoneElsesImport = mkFakeImport({ userID: 2, importID: "someone_elses" }); + await db.imports.insert(someoneElsesImport); const res = await mockApi @@ -145,6 +149,7 @@ t.test("POST /api/v1/imports/:importID/revert", async (t) => { t.resolveMatch( db.scores.findOne({ scoreID: FakeImport.scoreIDs[0] }), + // @ts-expect-error https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60020 null, "The scores that were part of this import should be deleted." diff --git a/server/src/server/router/api/v1/imports/router.ts b/server/src/server/router/api/v1/imports/router.ts index 4642866e9..2504cfa3b 100644 --- a/server/src/server/router/api/v1/imports/router.ts +++ b/server/src/server/router/api/v1/imports/router.ts @@ -10,6 +10,7 @@ import { RequirePermissions } from "server/middleware/auth"; import { GetRelevantSongsAndCharts } from "utils/db"; import { GetTachiData } from "utils/req-tachi-data"; import { GetUserWithID } from "utils/user"; +import type { ScoreImportWorkerReturns } from "lib/score-import/worker/types"; const router: Router = Router({ mergeParams: true }); @@ -157,7 +158,9 @@ router.get("/:importID/poll-status", async (req, res) => { description: `An internal service error has occured with this import. This has been reported!`, }); } else if (await job.isCompleted()) { - const content = await job.waitUntilFinished(ScoreImportQueueEvents); + const content = (await job.waitUntilFinished( + ScoreImportQueueEvents + )) as ScoreImportWorkerReturns; // Since job.isFailed() is for whether a job had a fatal exception // or not. We still want to check whether a job failed from say, diff --git a/server/src/server/router/api/v1/scores/_scoreID/router.test.ts b/server/src/server/router/api/v1/scores/_scoreID/router.test.ts index 5d5af6ee9..08c465c89 100644 --- a/server/src/server/router/api/v1/scores/_scoreID/router.test.ts +++ b/server/src/server/router/api/v1/scores/_scoreID/router.test.ts @@ -268,6 +268,7 @@ t.test("DELETE /api/v1/scores/:scoreID", (t) => { ); const someoneElsesScore = mkFakeScoreIIDXSP({ userID: 2, scoreID: "someone_elses" }); + await db.scores.insert(someoneElsesScore); const res = await mockApi @@ -280,6 +281,7 @@ t.test("DELETE /api/v1/scores/:scoreID", (t) => { t.resolveMatch( db.scores.findOne({ scoreID: someoneElsesScore.scoreID }), + // @ts-expect-error https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60020 null, "The score should be deleted." diff --git a/server/src/server/router/api/v1/scores/_scoreID/router.ts b/server/src/server/router/api/v1/scores/_scoreID/router.ts index fef039411..ee75bac93 100644 --- a/server/src/server/router/api/v1/scores/_scoreID/router.ts +++ b/server/src/server/router/api/v1/scores/_scoreID/router.ts @@ -158,7 +158,7 @@ router.delete( const score = GetTachiData(req, "scoreDoc"); - await DeleteScore(score, !!req.body.blacklist); + await DeleteScore(score, body.blacklist); return res.status(200).json({ success: true, diff --git a/server/src/server/router/api/v1/search/router.test.ts b/server/src/server/router/api/v1/search/router.test.ts index 7ec2d140d..86cd5965b 100644 --- a/server/src/server/router/api/v1/search/router.test.ts +++ b/server/src/server/router/api/v1/search/router.test.ts @@ -1,8 +1,8 @@ -import { SongDocument } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { LoadTachiIIDXData } from "test-utils/test-data"; +import type { SongDocument } from "tachi-common"; t.test("GET /api/v1/search", (t) => { t.beforeEach(ResetDBState); @@ -46,7 +46,9 @@ t.test("GET /api/v1/search/chart-hash", (t) => { const BMS_SHA2 = "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d"; const PMS_MD5 = "d1253dd56bb2087d0b0d474f0d562aae"; const PMS_SHA2 = "a10193f7ae05ce839292dc716f182fda0b1cc6ac5382c2056f37e22ffba87b7d"; - const USC_SHA1 = "USC_CHART_HASH"; // this is apparantly what our mock data uses. + + // this is apparantly what our mock data uses. + const USC_SHA1 = "USC_CHART_HASH"; t.test("Should return a BMS chart if the MD5 matches.", async (t) => { const res = await mockApi.get(`/api/v1/search/chart-hash?search=${BMS_MD5}`); diff --git a/server/src/server/router/api/v1/sessions/_sessionID/router.test.ts b/server/src/server/router/api/v1/sessions/_sessionID/router.test.ts index 3c6fedb90..c9e411322 100644 --- a/server/src/server/router/api/v1/sessions/_sessionID/router.test.ts +++ b/server/src/server/router/api/v1/sessions/_sessionID/router.test.ts @@ -1,6 +1,5 @@ -import t from "tap"; import db from "external/mongo/db"; - +import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { Testing511SPA } from "test-utils/test-data"; diff --git a/server/src/server/router/api/v1/sessions/_sessionID/router.ts b/server/src/server/router/api/v1/sessions/_sessionID/router.ts index f4519fdc9..2efd81de8 100644 --- a/server/src/server/router/api/v1/sessions/_sessionID/router.ts +++ b/server/src/server/router/api/v1/sessions/_sessionID/router.ts @@ -5,7 +5,6 @@ import { } from "./middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import p from "prudence"; import { RequirePermissions } from "server/middleware/auth"; import prValidate from "server/middleware/prudence-validate"; diff --git a/server/src/server/router/api/v1/status/router.test.ts b/server/src/server/router/api/v1/status/router.test.ts index 48cf61256..47111388f 100644 --- a/server/src/server/router/api/v1/status/router.test.ts +++ b/server/src/server/router/api/v1/status/router.test.ts @@ -1,7 +1,7 @@ import { VERSION_PRETTY } from "lib/constants/version"; +import t from "tap"; import { CreateFakeAuthCookie } from "test-utils/fake-auth"; import mockApi from "test-utils/mock-api"; -import t from "tap"; t.test("GET /api/v1/status", async (t) => { const cookie = await CreateFakeAuthCookie(mockApi); diff --git a/server/src/server/router/api/v1/users/_userID/api-tokens/router.test.ts b/server/src/server/router/api/v1/users/_userID/api-tokens/router.test.ts index c73b1b207..41561b56d 100644 --- a/server/src/server/router/api/v1/users/_userID/api-tokens/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/api-tokens/router.test.ts @@ -1,9 +1,9 @@ import db from "external/mongo/db"; -import { PublicUserDocument } 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 type { PublicUserDocument } from "tachi-common"; t.test("GET /api/v1/users/:userID/api-tokens", async (t) => { t.beforeEach(ResetDBState); diff --git a/server/src/server/router/api/v1/users/_userID/banner/router.ts b/server/src/server/router/api/v1/users/_userID/banner/router.ts index 73b5c7ea0..7091b4c10 100644 --- a/server/src/server/router/api/v1/users/_userID/banner/router.ts +++ b/server/src/server/router/api/v1/users/_userID/banner/router.ts @@ -4,7 +4,6 @@ import db from "external/mongo/db"; import { CDNDelete, CDNRedirect, CDNStoreOrOverwrite } from "lib/cdn/cdn"; import { GetProfileBannerURL } from "lib/cdn/url-format"; import { ONE_MEGABYTE } from "lib/constants/filesize"; -import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { RequirePermissions } from "server/middleware/auth"; import { CreateMulterSingleUploadMiddleware } from "server/middleware/multer-upload"; diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/_folderID/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/_folderID/router.test.ts index df9d5310f..8f186b58c 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/_folderID/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/_folderID/router.test.ts @@ -1,17 +1,17 @@ import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import { ChartDocument, FolderDocument, ScoreDocument, SongDocument } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { - GetKTDataJSON, - Testing511SPA, - TestingIIDXFolderSP10, - TestingIIDXSPScore, - TestingIIDXSPScorePB + GetKTDataJSON, + Testing511SPA, + TestingIIDXFolderSP10, + TestingIIDXSPScore, + TestingIIDXSPScorePB, } from "test-utils/test-data"; import { CreateFolderChartLookup } from "utils/folder"; +import type { ChartDocument, FolderDocument, ScoreDocument, SongDocument } from "tachi-common"; t.test("GET /api/v1/users/:userID/games/:game/:playtype/folders/:folderID", (t) => { t.beforeEach(ResetDBState); @@ -19,6 +19,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/folders/:folderID", (t) const folder = deepmerge(TestingIIDXFolderSP10, { folderID: "testing_folder", }) as FolderDocument; + await db.folders.insert(folder); await CreateFolderChartLookup(folder, true); await db["personal-bests"].insert(deepmerge(TestingIIDXSPScorePB, {})); @@ -65,8 +66,14 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/folders/:folderID/timeli await db.songs.iidx.remove({}); await db.charts.iidx.remove({}); await db["folder-chart-lookup"].remove({}); - await db.songs.iidx.insert(GetKTDataJSON("./tachi/tachi-songs-iidx.json") as Array>); - await db.charts.iidx.insert(GetKTDataJSON("./tachi/tachi-charts-iidx.json") as Array>); + await db.songs.iidx.insert( + GetKTDataJSON("./tachi/tachi-songs-iidx.json") as Array> + ); + await db.charts.iidx.insert( + GetKTDataJSON("./tachi/tachi-charts-iidx.json") as Array< + ChartDocument<"iidx:DP" | "iidx:SP"> + > + ); await CreateFolderChartLookup(folder, true); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/_folderID/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/_folderID/router.ts index c89fbf958..4ab7d5e76 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/_folderID/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/_folderID/router.ts @@ -155,37 +155,34 @@ router.get("/timeline", async (req, res) => { // Returns a unique score per-chart that was the first score to achieve // this criteria on that chart. - const scores = await db.scores - .aggregate([ - { - $match: matchCriteria, + const scoresAgg: Array<{ doc: ScoreDocument }> = await db.scores.aggregate([ + { + $match: matchCriteria, + }, + { + $addFields: { + __sortTime: { $ifNull: ["$timeAchieved", Infinity, "$timeAchieved"] }, }, - { - $addFields: { - __sortTime: { $ifNull: ["$timeAchieved", Infinity, "$timeAchieved"] }, - }, + }, + { + $sort: { + __sortTime: 1, }, - { - $sort: { - __sortTime: 1, - }, + }, + { + $group: { + _id: "$chartID", + doc: { $first: "$$ROOT" }, }, - { - $group: { - _id: "$chartID", - doc: { $first: "$$ROOT" }, - }, - }, - { - $unset: ["doc.__sortTime"], - }, - ]) - .then((r) => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (r.map((e: any) => e.doc) as Array).sort( - (a, b) => (a.timeAchieved ?? 0) - (b.timeAchieved ?? 0) - ) - ); + }, + { + $unset: ["doc.__sortTime"], + }, + ]); + + const scores = scoresAgg + .map((e) => e.doc) + .sort((a, b) => (a.timeAchieved ?? 0) - (b.timeAchieved ?? 0)); return res.status(200).json({ success: true, diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/router.ts index 29a6af326..58357efcc 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/folders/router.ts @@ -30,10 +30,15 @@ router.get("/", async (req, res) => { }); } + // if inactive is passed, we need this to be undefined so that + // mongodb returns both inactive and active folders. + // Otherwise, only return active folders. + const inactive = req.query.inactive === undefined ? false : undefined; + const folders = await SearchCollection( db.folders, req.query.search, - { game, playtype, inactive: !!req.query.inactive }, + { game, playtype, inactive }, 20 ); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.test.ts index 1404ae221..546cb4b79 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.test.ts @@ -1,5 +1,4 @@ import db from "external/mongo/db"; -import { ChartDocument, PBScoreDocument } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; @@ -10,12 +9,13 @@ import { Testing511SPA, TestingIIDXSPScorePB, } from "test-utils/test-data"; +import type { ChartDocument, PBScoreDocument } from "tachi-common"; t.test("GET /api/v1/users/:userID/games/:game/:playtype/pbs/best", (t) => { t.beforeEach(ResetDBState); t.test("Should return a user's best 100 personal bests.", async (t) => { - const mockPBs: PBScoreDocument[] = []; + const mockPBs: Array = []; for (let i = 0; i < 200; i++) { mockPBs.push({ @@ -23,7 +23,10 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/pbs/best", (t) => { game: "iidx", playtype: "SP", isPrimary: true, - chartID: i.toString(), // hack to generate some random chartIDs + + // hack to generate some random chartIDs + chartID: i.toString(), + songID: Testing511Song.id, calculatedData: { ktLampRating: i, @@ -34,7 +37,8 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/pbs/best", (t) => { await db["personal-bests"].insert(mockPBs); for (const sc of mockPBs) { - delete sc._id; // lol + // lol + delete sc._id; } const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/pbs/best"); @@ -87,15 +91,17 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/pbs", (t) => { }); t.test("Should search a user's personal bests.", async (t) => { - const mockPBs: PBScoreDocument[] = []; + const mockPBs: Array = []; const charts = GetKTDataJSON("./tachi/tachi-charts-iidx.json") as Array; for (let i = 0; i < 200; i++) { - let chart = charts[i]; + const chart = charts[i]; if (!chart) { - return t.fail(`Not enough charts in tachi-charts-iidx.json to mock pb data? Failed at index ${i}.`); + return t.fail( + `Not enough charts in tachi-charts-iidx.json to mock pb data? Failed at index ${i}.` + ); } mockPBs.push({ diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.test.ts index cacf69cfd..f9adbbb09 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.test.ts @@ -1,11 +1,11 @@ import db from "external/mongo/db"; -import { PublicUserDocument } from "tachi-common"; import t from "tap"; import { CreateFakeAuthCookie } from "test-utils/fake-auth"; import { mkFakeGameSettings, mkFakeUser } from "test-utils/misc"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { FakeOtherUser } from "test-utils/test-data"; +import type { PublicUserDocument } from "tachi-common"; const SetupRivals = async () => { await db.users.insert([FakeOtherUser, mkFakeUser(3), mkFakeUser(4)]); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts index e9ec94104..dda49e8c8 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.test.ts @@ -1,10 +1,10 @@ import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import { ScoreDocument } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { Testing511SPA, TestingIIDXSPScore, TestingIIDXSPScorePB } from "test-utils/test-data"; +import type { ScoreDocument } from "tachi-common"; t.test("GET /api/v1/users/:userID/games/:game/:playtype", (t) => { t.beforeEach(ResetDBState); @@ -65,7 +65,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype", (t) => { game: "iidx", playtype: "SP", }, - ] as ScoreDocument[]); + ] as Array); const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP"); @@ -117,6 +117,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/history", (t) => { userID: 1, game: "iidx", playtype: "SP", + // @ts-expect-error Too lazy to sort this one out... rankings: { BPI: { @@ -133,8 +134,9 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/history", (t) => { const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/history"); - res.body.body[0].timestamp = Math.floor(res.body.body[0].timestamp / 100_000); // by default, it's set to the current time. we can't + // by default, it's set to the current time. we can't // test that nicely, so lets round it to the nearest 100 seconds. + res.body.body[0].timestamp = Math.floor(res.body.body[0].timestamp / 100_000); t.strictSame(res.body.body, [ { @@ -151,6 +153,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/history", (t) => { playcount: 1, classes: {}, ratings: {}, + // close enough, right? timestamp: Math.floor(Date.now() / 100_000), }, @@ -181,6 +184,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/most-played", (t) => { await db.scores.insert([ // @ts-expect-error nonsense deepmerge(TestingIIDXSPScore, { scoreID: "something_else" }), + // @ts-expect-error nonsense deepmerge(TestingIIDXSPScore, { scoreID: "something_else2" }), ]); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/scores/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/scores/router.test.ts index be30ee756..a0d042f41 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/scores/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/scores/router.test.ts @@ -1,21 +1,21 @@ import db from "external/mongo/db"; -import { ChartDocument, ScoreDocument } from "tachi-common"; import t from "tap"; import { mkFakeScoreIIDXSP, mkFakeScoreSDVX } from "test-utils/misc"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { - GetKTDataJSON, - LoadTachiIIDXData, - Testing511Song, - Testing511SPA + GetKTDataJSON, + LoadTachiIIDXData, + Testing511Song, + Testing511SPA, } from "test-utils/test-data"; +import type { ChartDocument, ScoreDocument } from "tachi-common"; t.test("GET /api/v1/users/:userID/games/:game/:playtype/scores/recent", (t) => { t.beforeEach(ResetDBState); t.test("Should return a users 100 most recent scores.", async (t) => { - const mockScores: ScoreDocument[] = []; + const mockScores: Array = []; for (let i = 0; i < 200; i++) { mockScores.push({ @@ -32,7 +32,8 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/scores/recent", (t) => { await db.scores.insert(mockScores); for (const sc of mockScores) { - delete sc._id; // lol + // lol + delete sc._id; } const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/scores/recent"); @@ -86,15 +87,19 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/scores", (t) => { }); t.test("Should search a user's scores.", async (t) => { - const mockScores: ScoreDocument[] = []; + const mockScores: Array = []; - const charts = GetKTDataJSON("./tachi/tachi-charts-iidx.json") as Array>; + const charts = GetKTDataJSON("./tachi/tachi-charts-iidx.json") as Array< + ChartDocument<"iidx:DP" | "iidx:SP"> + >; for (let i = 0; i < 200; i++) { const chart = charts[i]; if (!chart) { - return t.fail(`tachi-charts-iidx.json doesn't have enough mock data for testing. Needed atleast 200 entries, but failed to retrieve one at index ${i}.`); + return t.fail( + `tachi-charts-iidx.json doesn't have enough mock data for testing. Needed atleast 200 entries, but failed to retrieve one at index ${i}.` + ); } mockScores.push({ @@ -208,7 +213,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/scores/:chartID", (t) => userID: 2, scoreID: "baz", }, - ] as ScoreDocument[]); + ] as Array); const res = await mockApi.get( `/api/v1/users/1/games/iidx/SP/scores/${Testing511SPA.chartID}` diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/sessions/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/sessions/router.test.ts index a75047185..f99b41da6 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/sessions/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/sessions/router.test.ts @@ -1,9 +1,9 @@ import db from "external/mongo/db"; -import { SessionDocument } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { LoadTachiIIDXData } from "test-utils/test-data"; +import type { SessionDocument } from "tachi-common"; t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions", (t) => { t.beforeEach(ResetDBState); @@ -46,9 +46,11 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions", (t) => { playtype: "SP", name: e, desc: "something", - sessionID: e, // hack to avoid db nonsense + + // hack to avoid db nonsense + sessionID: e, }) - ) as SessionDocument[] + ) as Array ); const res = await mockApi.get( @@ -72,7 +74,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions/best", (t) => { t.beforeEach(LoadTachiIIDXData); t.test("Should return a user's best 100 sessions.", async (t) => { - const sessions: SessionDocument[] = []; + const sessions: Array = []; for (let i = 0; i < 200; i++) { sessions.push({ @@ -110,7 +112,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions/best", (t) => { t.test( "Should return a user's best 100 sessions according to the provided rating alg.", async (t) => { - const sessions: SessionDocument[] = []; + const sessions: Array = []; for (let i = 0; i < 200; i++) { sessions.push({ @@ -205,12 +207,13 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions/recent", (t) => sessionID: "recent_id3", timeEnded: 2, }, - ] as SessionDocument[]); + ] as Array); const res = await mockApi.get("/api/v1/users/1/games/iidx/SP/sessions/recent"); t.equal(res.body.body.length, 3); t.strictSame( + // eslint-disable-next-line lines-around-comment // @ts-expect-error temporary type hack res.body.body.map((e) => e.sessionID), ["recent_id2", "recent_id3", "recent_id1"] diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.test.ts index db54d6f04..40509c96f 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.test.ts @@ -1,9 +1,9 @@ /* eslint-disable no-await-in-loop */ import db from "external/mongo/db"; -import { UGPTSettings } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; +import type { UGPTSettings } from "tachi-common"; t.test("GET /api/v1/users/:userID/games/:game/:playtype/settings", (t) => { t.beforeEach(ResetDBState); @@ -200,7 +200,7 @@ t.test("PATCH /api/v1/users/:userID/games/:game/:playtype/settings", (t) => { userID: 1, game: "iidx", playtype: "SP", - })) as UGPTSettings<"iidx:SP" | "iidx:DP"> | null; + })) as UGPTSettings<"iidx:DP" | "iidx:SP"> | null; t.equal(data?.preferences.gameSpecific.bpiTarget, target); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/showcase/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/showcase/router.test.ts index 2d120a996..d5ba4d7e0 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/showcase/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/showcase/router.test.ts @@ -1,11 +1,12 @@ import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import { ChartDocument, IIDX_GRADES, IIDX_LAMPS, PublicUserDocument } from "tachi-common"; +import { IIDX_GRADES, IIDX_LAMPS } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { Testing511SPA, TestingIIDXFolderSP10, TestingIIDXSPScorePB } from "test-utils/test-data"; import { CreateFolderChartLookup } from "utils/folder"; +import type { ChartDocument, PublicUserDocument } from "tachi-common"; const SetFolders = async () => { await db.folders.insert(TestingIIDXFolderSP10); @@ -233,7 +234,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/showcase/custom", (t) => t.equal(res.statusCode, 400); const res2 = await mockApi.get( - `/api/v1/users/1/games/iidx/SP/showcase/custom?mode=folder&property=grade>e=4&folderID=${TestingIIDXFolderSP10},invalid` + `/api/v1/users/1/games/iidx/SP/showcase/custom?mode=folder&property=grade>e=4&folderID=${TestingIIDXFolderSP10.folderID},invalid` ); t.equal(res2.statusCode, 400); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/tables/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/tables/router.test.ts index 77b01499a..375c9cee0 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/tables/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/tables/router.test.ts @@ -1,11 +1,11 @@ import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import { FolderDocument } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { TestingIIDXFolderSP10, TestingIIDXSPScorePB } from "test-utils/test-data"; import { CreateFolderChartLookup } from "utils/folder"; +import type { FolderDocument } from "tachi-common"; t.test("GET /api/v1/users/:userID/games/:game/:playtype/tables/:tableID", (t) => { t.beforeEach(ResetDBState); @@ -14,6 +14,7 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/tables/:tableID", (t) => const folder = deepmerge(TestingIIDXFolderSP10, { folderID: "testing_folder", }) as FolderDocument; + await db.folders.insert(folder); await CreateFolderChartLookup(folder, true); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.test.ts index 31a397b01..58492208b 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.test.ts @@ -1,34 +1,30 @@ /* eslint-disable no-await-in-loop */ import dm from "deepmerge"; import db from "external/mongo/db"; -import { - ChartDocument, - GoalSubscriptionDocument, - IIDX_GRADES, - IIDX_LAMPS, - PBScoreDocument -} from "tachi-common"; +import { IIDX_GRADES, IIDX_LAMPS } 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 { TestSnapshot } from "test-utils/single-process-snapshot"; import { - HC511Goal, - HC511UserGoal, - IIDXSPMilestoneGoals, - IIDXSPMilestoneGoalSubs, - Testing511SPA, - TestingIIDXFolderSP10, - TestingIIDXSPMilestone, - TestingIIDXSPMilestoneSub, - TestingIIDXSPScore, - TestingIIDXSPScorePB + HC511Goal, + HC511UserGoal, + IIDXSPMilestoneGoals, + IIDXSPMilestoneGoalSubs, + Testing511SPA, + TestingIIDXFolderSP10, + TestingIIDXSPMilestone, + TestingIIDXSPMilestoneSub, + TestingIIDXSPScore, + TestingIIDXSPScorePB, } from "test-utils/test-data"; import { CreateFolderChartLookup } from "utils/folder"; +import type { ChartDocument, GoalSubscriptionDocument, PBScoreDocument } from "tachi-common"; // @ts-expect-error Not sure why the types break here, but they do. const dupedGoal = dm({}, HC511Goal); + // @ts-expect-error Not sure why the types break here, but they do. const dupedGoalSub = dm({}, HC511UserGoal); @@ -108,6 +104,7 @@ t.test("POST /api/v1/users/:userID/games/:game/:playtype/targets/add-goal", asyn } t.beforeEach(() => db.charts.iidx.insert(anotherFakeChart)); + // @ts-expect-error weird type issues t.beforeEach(() => db.folders.insert(dm(TestingIIDXFolderSP10, {}))); @@ -170,6 +167,7 @@ t.test("POST /api/v1/users/:userID/games/:game/:playtype/targets/add-goal", asyn criteria: proportionModeCriteria, charts: multiCharts, }), + // NOT LEGAL FOR IIDX, due to being nonsense! NEEDS SPECIFIC TESTING. // mkInput("Score:Multi Case", { // criteria: { @@ -235,6 +233,7 @@ t.test("POST /api/v1/users/:userID/games/:game/:playtype/targets/add-goal", asyn criteria: proportionModeCriteria, charts: anyCharts, }), + // also illegal in iidx, bms and pms. // mkInput("Score:Any Case", { // criteria: { @@ -300,6 +299,7 @@ t.test("POST /api/v1/users/:userID/games/:game/:playtype/targets/add-goal", asyn criteria: proportionModeCriteria, charts: folderCharts, }), + // also illegal in iidx, bms and pms. // mkInput("Score:Folder Case", { // criteria: { @@ -364,6 +364,7 @@ t.test("POST /api/v1/users/:userID/games/:game/:playtype/targets/add-goal", asyn .send(input.content); t.equal( + // eslint-disable-next-line lines-around-comment // this looks stupid, but results in better errmsgs. res.statusCode === 200 ? 200 : res.body.description, 200, diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/milestones/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/milestones/router.test.ts index 73ea43eea..60f4e495f 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/milestones/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/milestones/router.test.ts @@ -1,15 +1,15 @@ import db from "external/mongo/db"; -import { PublicUserDocument, 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 { - IIDXSPMilestoneGoals, - IIDXSPMilestoneGoalSubs, - TestingIIDXSPMilestone, - TestingIIDXSPMilestoneSub + IIDXSPMilestoneGoals, + IIDXSPMilestoneGoalSubs, + TestingIIDXSPMilestone, + TestingIIDXSPMilestoneSub, } from "test-utils/test-data"; +import type { PublicUserDocument, UserGameStats } from "tachi-common"; t.test("GET /api/v1/users/:userID/games/:game/:playtype/targets/milestones", (t) => { t.beforeEach(ResetDBState); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/router.test.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/router.test.ts index fbf7c6b11..8608e2943 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/router.test.ts @@ -1,11 +1,5 @@ import dm from "deepmerge"; import db from "external/mongo/db"; -import { - GoalDocument, - GoalSubscriptionDocument, - MilestoneDocument, - MilestoneSubscriptionDocument, -} from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; @@ -15,6 +9,12 @@ import { TestingIIDXSPMilestone, TestingIIDXSPMilestoneSub, } from "test-utils/test-data"; +import type { + GoalDocument, + GoalSubscriptionDocument, + MilestoneDocument, + MilestoneSubscriptionDocument, +} from "tachi-common"; function mkGoalSub(merge: Partial) { return dm(HC511UserGoal, merge); diff --git a/server/src/server/router/api/v1/users/_userID/imports/router.test.ts b/server/src/server/router/api/v1/users/_userID/imports/router.test.ts index 8cea9b672..bd3010b7e 100644 --- a/server/src/server/router/api/v1/users/_userID/imports/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/imports/router.test.ts @@ -1,10 +1,10 @@ import db from "external/mongo/db"; -import { ImportDocument } from "tachi-common"; import t from "tap"; import { mkFakeImport } from "test-utils/misc"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { ApplyNTimes } from "utils/misc"; +import type { ImportDocument } from "tachi-common"; t.test("GET /api/v1/users/:userID/imports", (t) => { t.beforeEach(ResetDBState); diff --git a/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts index fc5db6b54..074b32071 100644 --- a/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.test.ts @@ -1,9 +1,9 @@ import db from "external/mongo/db"; -import { PublicUserDocument } 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 type { PublicUserDocument } from "tachi-common"; t.test("GET /api/v1/users/:userID/integrations/fervidex/s", async (t) => { t.beforeEach(ResetDBState); diff --git a/server/src/server/router/api/v1/users/_userID/integrations/kai/_kaiType/middleware.test.ts b/server/src/server/router/api/v1/users/_userID/integrations/kai/_kaiType/middleware.test.ts index 762f9fd96..63d2e9ded 100644 --- a/server/src/server/router/api/v1/users/_userID/integrations/kai/_kaiType/middleware.test.ts +++ b/server/src/server/router/api/v1/users/_userID/integrations/kai/_kaiType/middleware.test.ts @@ -1,6 +1,6 @@ +import { ValidateKaiType } from "./middleware"; import expMiddlewareMock from "express-request-mock"; import t from "tap"; -import { ValidateKaiType } from "./middleware"; t.test("#ValidateKaiType", (t) => { const k = async (k: string) => { diff --git a/server/src/server/router/api/v1/users/_userID/integrations/kai/_kaiType/router.ts b/server/src/server/router/api/v1/users/_userID/integrations/kai/_kaiType/router.ts index e23ae24fd..56b33ad4b 100644 --- a/server/src/server/router/api/v1/users/_userID/integrations/kai/_kaiType/router.ts +++ b/server/src/server/router/api/v1/users/_userID/integrations/kai/_kaiType/router.ts @@ -2,7 +2,6 @@ import { ValidateKaiType } from "./middleware"; import { RequireSelfRequestFromUser } from "../../../middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { GetKaiTypeClientCredentials, diff --git a/server/src/server/router/api/v1/users/_userID/invites/router.test.ts b/server/src/server/router/api/v1/users/_userID/invites/router.test.ts index 61820a75a..876c2fd53 100644 --- a/server/src/server/router/api/v1/users/_userID/invites/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/invites/router.test.ts @@ -1,10 +1,10 @@ import db from "external/mongo/db"; import { ONE_MONTH } from "lib/constants/time"; -import { InviteCodeDocument, PublicUserDocument } 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 type { InviteCodeDocument, PublicUserDocument } from "tachi-common"; t.test("GET /api/v1/users/:userID/invites", async (t) => { t.beforeEach(ResetDBState); @@ -21,7 +21,7 @@ t.test("GET /api/v1/users/:userID/invites", async (t) => { const res = await mockApi.get("/api/v1/users/1/invites").set("Cookie", cookie); t.strictSame( - (res.body.body.invites as InviteCodeDocument[]).sort( + (res.body.body.invites as Array).sort( (a, b) => a.createdAt - b.createdAt ), [ @@ -130,6 +130,7 @@ t.test("POST /api/v1/users/:userID/invites/create", async (t) => { t.test("Should honor invite limit.", async (t) => { await db.invites.remove({}); + // users with less than a month of life in them dont get invites, // so this will force an invite limit honor. await db.users.update({ id: 1 }, { $set: { joinDate: Date.now() } }); diff --git a/server/src/server/router/api/v1/users/_userID/invites/router.ts b/server/src/server/router/api/v1/users/_userID/invites/router.ts index 6089698b4..9a842b377 100644 --- a/server/src/server/router/api/v1/users/_userID/invites/router.ts +++ b/server/src/server/router/api/v1/users/_userID/invites/router.ts @@ -1,7 +1,6 @@ import { RequireSelfRequestFromUser } from "../middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { GetTotalAllowedInvites } from "lib/invites/invites"; import { RequireKamaitachi } from "server/middleware/type-require"; import { Random20Hex } from "utils/misc"; diff --git a/server/src/server/router/api/v1/users/_userID/middleware.ts b/server/src/server/router/api/v1/users/_userID/middleware.ts index 91bdc1602..02466e1a9 100644 --- a/server/src/server/router/api/v1/users/_userID/middleware.ts +++ b/server/src/server/router/api/v1/users/_userID/middleware.ts @@ -1,9 +1,8 @@ -import { SYMBOL_TACHI_API_AUTH, SYMBOL_TACHI_DATA } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import { IsNullish } from "utils/misc"; import { AssignToReqTachiData, GetTachiData } from "utils/req-tachi-data"; import { ResolveUser } from "utils/user"; import type { RequestHandler } from "express"; -import type { integer } from "tachi-common"; export const GetUserFromParam: RequestHandler = async (req, res, next) => { if (!req.params.userID) { diff --git a/server/src/server/router/api/v1/users/_userID/notifications/router.test.ts b/server/src/server/router/api/v1/users/_userID/notifications/router.test.ts index d6439dbf0..6f5fb41f1 100644 --- a/server/src/server/router/api/v1/users/_userID/notifications/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/notifications/router.test.ts @@ -1,10 +1,10 @@ import db from "external/mongo/db"; -import { NotificationDocument } from "tachi-common"; import t from "tap"; import { CreateFakeAuthCookie } from "test-utils/fake-auth"; import { mkFakeNotification, mkFakeUser } from "test-utils/misc"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; +import type { NotificationDocument } from "tachi-common"; t.test("GET /api/v1/users/:userID/notifications", async (t) => { t.beforeEach(ResetDBState); diff --git a/server/src/server/router/api/v1/users/_userID/notifications/router.ts b/server/src/server/router/api/v1/users/_userID/notifications/router.ts index 8d0e0d344..0cd4947f0 100644 --- a/server/src/server/router/api/v1/users/_userID/notifications/router.ts +++ b/server/src/server/router/api/v1/users/_userID/notifications/router.ts @@ -1,7 +1,6 @@ import { RequireSelfRequestFromUser } from "../middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { ONE_SECOND } from "lib/constants/time"; import { GetTachiData } from "utils/req-tachi-data"; diff --git a/server/src/server/router/api/v1/users/_userID/pfp/router.test.ts b/server/src/server/router/api/v1/users/_userID/pfp/router.test.ts index 6bb0ed61c..d3a317f4f 100644 --- a/server/src/server/router/api/v1/users/_userID/pfp/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/pfp/router.test.ts @@ -11,6 +11,7 @@ t.test("GET /api/v1/users/:userID/pfp", (t) => { t.test("Should return the default profile picture if user has no custom pfp", async (t) => { await CDNStoreOrOverwrite("/users/default/pfp", "test"); + // we have to follow redirs here lol const res = await mockApi.get("/api/v1/users/1/pfp").redirects(1); diff --git a/server/src/server/router/api/v1/users/_userID/pfp/router.ts b/server/src/server/router/api/v1/users/_userID/pfp/router.ts index 4e37a86bc..e3981a522 100644 --- a/server/src/server/router/api/v1/users/_userID/pfp/router.ts +++ b/server/src/server/router/api/v1/users/_userID/pfp/router.ts @@ -4,7 +4,6 @@ import db from "external/mongo/db"; import { CDNDelete, CDNRedirect, CDNStoreOrOverwrite } from "lib/cdn/cdn"; import { GetProfilePictureURL } from "lib/cdn/url-format"; import { ONE_MEGABYTE } from "lib/constants/filesize"; -import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { RequirePermissions } from "server/middleware/auth"; import { CreateMulterSingleUploadMiddleware } from "server/middleware/multer-upload"; diff --git a/server/src/server/router/api/v1/users/_userID/router.test.ts b/server/src/server/router/api/v1/users/_userID/router.test.ts index 124f5d041..cd2a96bec 100644 --- a/server/src/server/router/api/v1/users/_userID/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/router.test.ts @@ -1,12 +1,12 @@ +import { PasswordCompare } from "../../auth/auth"; import db from "external/mongo/db"; import { ONE_DAY, ONE_YEAR } from "lib/constants/time"; -import { Game, ImportDocument, ImportTypes, 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 { Random20Hex } from "utils/misc"; -import { PasswordCompare } from "../../auth/auth"; +import type { Game, ImportDocument, ImportTypes, UserGameStats } from "tachi-common"; t.test("GET /api/v1/users/:userID", (t) => { t.beforeEach(ResetDBState); @@ -314,7 +314,7 @@ t.test("GET /api/v1/users/:userID/game-stats", (t) => { t.test("Should return all of a user's game stats.", async (t) => { await db["game-stats"].remove({}); - const stats: UserGameStats[] = [ + const stats: Array = [ { userID: 1, game: "iidx", diff --git a/server/src/server/router/api/v1/users/_userID/settings/router.test.ts b/server/src/server/router/api/v1/users/_userID/settings/router.test.ts index f68fa4f9b..35ff16371 100644 --- a/server/src/server/router/api/v1/users/_userID/settings/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/settings/router.test.ts @@ -1,10 +1,10 @@ import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import { PublicUserDocument } 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 type { PublicUserDocument } from "tachi-common"; t.test("GET /api/v1/users/:userID/settings", (t) => { t.beforeEach(ResetDBState); @@ -119,6 +119,7 @@ t.test("PATCH /api/v1/users/:userID/settings", async (t) => { t.test("Must be authenticated as that user.", async (t) => { const user = await db.users.findOne({}); + await db.users.insert( deepmerge(user!, { id: 2, @@ -129,7 +130,9 @@ t.test("PATCH /api/v1/users/:userID/settings", async (t) => { const res = await mockApi .patch("/api/v1/users/2/settings") - .set("Cookie", cookie) // this token is for user 1, not 2 + + // this token is for user 1, not 2 + .set("Cookie", cookie) .send({ developerMode: false, }); diff --git a/server/src/server/router/api/v1/users/router.test.ts b/server/src/server/router/api/v1/users/router.test.ts index 9cf4d194c..add146e58 100644 --- a/server/src/server/router/api/v1/users/router.test.ts +++ b/server/src/server/router/api/v1/users/router.test.ts @@ -1,8 +1,8 @@ -import t from "tap"; import db from "external/mongo/db"; +import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; -import { PublicUserDocument } from "tachi-common"; +import type { PublicUserDocument } from "tachi-common"; t.test("GET /api/v1/users", (t) => { t.beforeEach(ResetDBState); diff --git a/server/src/server/router/ir/beatoraja/auth.test.ts b/server/src/server/router/ir/beatoraja/auth.test.ts index c69eb67a6..cd1f47df9 100644 --- a/server/src/server/router/ir/beatoraja/auth.test.ts +++ b/server/src/server/router/ir/beatoraja/auth.test.ts @@ -1,6 +1,6 @@ +import { ValidateIRClientVersion } from "./auth"; import expMiddlewareMock from "express-request-mock"; import t from "tap"; -import { ValidateIRClientVersion } from "./auth"; t.test("#ValidateIRClientVersion", (t) => { t.test("Should reject clients that are not supported", async (t) => { diff --git a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.test.ts b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.test.ts index 47f902e5b..c4a673f88 100644 --- a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.test.ts +++ b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.test.ts @@ -1,9 +1,9 @@ +import { TachiScoreDataToBeatorajaFormat } from "./convert-scores"; import deepmerge from "deepmerge"; -import { PBScoreDocument } from "tachi-common"; import t from "tap"; import ResetDBState from "test-utils/resets"; import { BMSGazerChart, GetKTDataJSON } from "test-utils/test-data"; -import { TachiScoreDataToBeatorajaFormat } from "./convert-scores"; +import type { PBScoreDocument } from "tachi-common"; const pbScore = { composedFrom: { diff --git a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.test.ts b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.test.ts index 8f116d5f9..f65f61088 100644 --- a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.test.ts +++ b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.test.ts @@ -1,8 +1,8 @@ import db from "external/mongo/db"; -import { PBScoreDocument, ScoreDocument } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; +import type { PBScoreDocument, ScoreDocument } from "tachi-common"; t.test("GET /ir/beatoraja/charts/:chartSHA256/scores", (t) => { t.beforeEach(ResetDBState); @@ -22,6 +22,7 @@ t.test("GET /ir/beatoraja/charts/:chartSHA256/scores", (t) => { const GAZER_CHARTID = "88eb6cc5683e2740cbd07f588a5f3db1db8d467b"; t.test("Should return PB scores on a chart", async (t) => { + // very lazy fake scores await db["personal-bests"].insert({ composedFrom: { lampPB: "mock_lampPB", @@ -34,7 +35,7 @@ t.test("GET /ir/beatoraja/charts/:chartSHA256/scores", (t) => { scoreMeta: {}, chartID: GAZER_CHARTID, userID: 1, - } as unknown as PBScoreDocument); // very lazy fake scores + } as unknown as PBScoreDocument); await db.scores.insert({ scoreID: "mock_lampPB", diff --git a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.ts b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.ts index e59fb25aa..7b06d29e4 100644 --- a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.ts +++ b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.ts @@ -1,13 +1,16 @@ import { TachiScoreDataToBeatorajaFormat } from "./convert-scores"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TACHI_API_AUTH, SYMBOL_TACHI_DATA } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; +import CreateLogCtx from "lib/logger/logger"; import { AssignToReqTachiData, GetTachiData } from "utils/req-tachi-data"; import type { RequestHandler } from "express"; -import type { ChartDocument, PBScoreDocument } from "tachi-common"; +import type { ChartDocument, integer, PBScoreDocument, PublicUserDocument } from "tachi-common"; const router: Router = Router({ mergeParams: true }); +const logger = CreateLogCtx(__filename); + const GetChartDocument: RequestHandler = async (req, res, next) => { let chart: ChartDocument<"bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard"> | null = (await db.charts.bms.findOne({ @@ -61,7 +64,7 @@ router.get("/scores", async (req, res) => { }, } ); - const userMap = new Map(); + const userMap = new Map(); for (const user of userDocs) { userMap.set(user.id, user); @@ -70,13 +73,24 @@ router.get("/scores", async (req, res) => { const beatorajaScores = []; for (const score of scores) { + const username = userMap.get(score.userID)?.username; + + if (!username) { + logger.warn( + `A PB on ${score.chartID} refers to user ${score.userID}, who apparantly doesn't exist? Skipping for beatoraja score returns, but this might be severe!` + ); + continue; + } + beatorajaScores.push( TachiScoreDataToBeatorajaFormat( score, chart.data.hashSHA256, - score.userID === requestingUserID ? "" : userMap.get(score.userID).username, + score.userID === requestingUserID ? "" : username, chart.data.notecount, - 0 // Playcount is always 0 at the moment due to performance concerns. + + // Playcount is always 0 at the moment due to performance concerns. + 0 ) ); } diff --git a/server/src/server/router/ir/beatoraja/router.test.ts b/server/src/server/router/ir/beatoraja/router.test.ts index 246c0872f..693322408 100644 --- a/server/src/server/router/ir/beatoraja/router.test.ts +++ b/server/src/server/router/ir/beatoraja/router.test.ts @@ -1,10 +1,10 @@ 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, MockBeatorajaBMSScore, MockBeatorajaPMSScore } from "test-utils/test-data"; +import type { PublicUserDocument } from "tachi-common"; t.test("POST /ir/beatoraja/submit-score", (t) => { t.beforeEach(ResetDBState); @@ -272,7 +272,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => { username: "bar", usernameLowercase: "bar", }, - ] as PublicUserDocument[]); + ] as Array); const res = await mockApi .post("/ir/beatoraja/submit-score") diff --git a/server/src/server/router/ir/direct-manual/router.test.ts b/server/src/server/router/ir/direct-manual/router.test.ts index 8a4d1b61f..ce4f9f150 100644 --- a/server/src/server/router/ir/direct-manual/router.test.ts +++ b/server/src/server/router/ir/direct-manual/router.test.ts @@ -1,11 +1,15 @@ import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import { BatchManual, BatchManualScore } 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 { FakeChunitachiBatchManual, FakeSmallBatchManual, GetKTDataJSON } from "test-utils/test-data"; +import { + FakeChunitachiBatchManual, + FakeSmallBatchManual, + GetKTDataJSON, +} from "test-utils/test-data"; +import type { BatchManual, BatchManualScore } from "tachi-common"; t.test("POST /ir/direct-manual/import", async (t) => { const cookie = await CreateFakeAuthCookie(mockApi); @@ -60,9 +64,7 @@ t.test("POST /ir/direct-manual/import", async (t) => { }); t.test("Should require authentication.", async (t) => { - const res = await mockApi - .post("/ir/direct-manual/import") - .send(FakeSmallBatchManual); + const res = await mockApi.post("/ir/direct-manual/import").send(FakeSmallBatchManual); t.equal(res.statusCode, 401); diff --git a/server/src/server/router/ir/fervidex/router.test.ts b/server/src/server/router/ir/fervidex/router.test.ts index 18d5cf18b..0f5d0485a 100644 --- a/server/src/server/router/ir/fervidex/router.test.ts +++ b/server/src/server/router/ir/fervidex/router.test.ts @@ -20,6 +20,7 @@ function TestHeaders(url: string, data: any) { const res = await mockApi .post(url) .set("Authorization", "Bearer mock_token") + // rootage .set("X-Software-Model", "LDJ:J:B:A:2020092900") .set("X-Account-Id", "bar") @@ -31,6 +32,7 @@ function TestHeaders(url: string, data: any) { const res2 = await mockApi .post(url) .set("Authorization", "Bearer mock_token") + // rootage .set("X-Software-Model", "LDJ:J:B:A:2020092900") .set("User-Agent", "fervidex/1.3.0") @@ -45,6 +47,7 @@ function TestHeaders(url: string, data: any) { let res = await mockApi .post(url) .set("Authorization", "Bearer mock_token") + // rootage .set("X-Software-Model", "LDJ:J:B:A:2019090200") .set("User-Agent", "fervidex/1.3.0") @@ -55,6 +58,7 @@ function TestHeaders(url: string, data: any) { res = await mockApi .post(url) .set("Authorization", "Bearer mock_token") + // rootage old .set("X-Software-Model", "LDJ:J:B:A:2019100700") .set("User-Agent", "fervidex/1.3.0") @@ -65,6 +69,7 @@ function TestHeaders(url: string, data: any) { res = await mockApi .post(url) .set("Authorization", "Bearer mock_token") + // cannonballers .set("User-Agent", "fervidex/1.3.0") .set("X-Software-Model", "LDJ:J:B:A:2018091900") @@ -84,6 +89,7 @@ function TestHeaders(url: string, data: any) { res = await mockApi .post(url) .set("Authorization", "Bearer mock_token") + // BMS-iidx .set("User-Agent", "fervidex/1.3.0") .set("X-Software-Model", "LDJ:J:B:Z:2020092900") @@ -94,6 +100,7 @@ function TestHeaders(url: string, data: any) { res = await mockApi .post(url) .set("Authorization", "Bearer mock_token") + // BMS-iidx .set("User-Agent", "fervidex/1.2.0") .set("X-Software-Model", "LDJ:J:B:A:2020092900") @@ -104,6 +111,7 @@ function TestHeaders(url: string, data: any) { res = await mockApi .post(url) .set("Authorization", "Bearer mock_token") + // BMS-iidx .set("User-Agent", "fervidex/.0") .set("X-Software-Model", "LDJ:J:B:A:2020092900") @@ -114,6 +122,7 @@ function TestHeaders(url: string, data: any) { res = await mockApi .post(url) .set("Authorization", "Bearer mock_token") + // BMS-iidx .set("User-Agent", "") .set("X-Software-Model", "LDJ:J:B:A:2020092900") @@ -124,6 +133,7 @@ function TestHeaders(url: string, data: any) { res = await mockApi .post(url) .set("Authorization", "Bearer mock_token") + // BMS-iidx .set("User-Agent", "invalid") .set("X-Software-Model", "LDJ:J:B:A:2020092900") diff --git a/server/src/server/router/ir/lr2hook/router.test.ts b/server/src/server/router/ir/lr2hook/router.test.ts index 99ca9f263..1ad596095 100644 --- a/server/src/server/router/ir/lr2hook/router.test.ts +++ b/server/src/server/router/ir/lr2hook/router.test.ts @@ -1,11 +1,11 @@ import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import { LR2HookScore } from "lib/score-import/import-types/ir/lr2hook/types"; import t from "tap"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; import { TestingLR2HookScore } from "test-utils/test-data"; import { ApplyNTimes, RFA } from "utils/misc"; +import type { LR2HookScore } from "lib/score-import/import-types/ir/lr2hook/types"; t.test("POST /ir/lr2hook/import", (t) => { t.beforeEach(ResetDBState); diff --git a/server/src/server/router/ir/lr2hook/router.ts b/server/src/server/router/ir/lr2hook/router.ts index 0ff74e270..a702e4492 100644 --- a/server/src/server/router/ir/lr2hook/router.ts +++ b/server/src/server/router/ir/lr2hook/router.ts @@ -2,7 +2,7 @@ import { Router } from "express"; import db from "external/mongo/db"; import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper"; -import { PR_LR2Hook as PR_LR2_HOOK } from "lib/score-import/import-types/ir/lr2hook/parser"; +import { PR_LR2HOOK as PR_LR2_HOOK } from "lib/score-import/import-types/ir/lr2hook/parser"; import { RequirePermissions } from "server/middleware/auth"; import prValidate from "server/middleware/prudence-validate"; import { UpdateClassIfGreater } from "utils/class"; diff --git a/server/src/server/router/ir/usc/_playtype/router.test.ts b/server/src/server/router/ir/usc/_playtype/router.test.ts index dceb5d752..955e22ba4 100644 --- a/server/src/server/router/ir/usc/_playtype/router.test.ts +++ b/server/src/server/router/ir/usc/_playtype/router.test.ts @@ -1,11 +1,11 @@ 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 { GetKTDataBuffer } from "test-utils/test-data"; +import type { PBScoreDocument, PublicUserDocument, ScoreDocument } from "tachi-common"; async function InsertFakeUSCAuth() { await db["api-tokens"].insert({ @@ -154,7 +154,10 @@ const USC_SCORE_PB: PBScoreDocument = { esd: null, lamp: "EXCESSIVE CLEAR", lampIndex: 2, - gradeIndex: 4, // idk, lazy + + // idk, lazy + gradeIndex: 4, + judgements: { critical: 50, near: 30, @@ -322,7 +325,7 @@ t.test("GET /charts/:chartHash/leaderboard", (t) => { scoreID: "other_usc_score_pb", scoreMeta: { noteMod: "NORMAL", gaugeMod: "HARD" }, }, - ] as ScoreDocument[]); + ] as Array); const res = await mockApi .get("/ir/usc/Controller/charts/USC_CHART_HASH/leaderboard?mode=best&n=2") @@ -461,6 +464,7 @@ t.test("POST /replays", (t) => { const res = await mockApi .post("/ir/usc/Controller/replays") .field("identifier", "MOCK_IDENTIFIER") + // .attach("replay", replayFile, "replay.urf") .set("Authorization", "Bearer foo"); @@ -662,7 +666,7 @@ t.test("POST /scores", (t) => { username: "bar", usernameLowercase: "bar", }, - ] as PublicUserDocument[]); + ] as Array); const res = await mockApi .post("/ir/usc/Controller/scores") @@ -776,7 +780,7 @@ t.test("POST /scores", (t) => { username: "baz", usernameLowercase: "baz", }, - ] as PublicUserDocument[]); + ] as Array); const res = await mockApi .post("/ir/usc/Controller/scores") diff --git a/server/src/server/router/ir/usc/_playtype/usc.test.ts b/server/src/server/router/ir/usc/_playtype/usc.test.ts index 92376118d..6a716cdd6 100644 --- a/server/src/server/router/ir/usc/_playtype/usc.test.ts +++ b/server/src/server/router/ir/usc/_playtype/usc.test.ts @@ -1,9 +1,14 @@ +import { CreatePOSTScoresResponseBody, TachiScoreToServerScore } from "./usc"; 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 type { + ChartDocument, + PBScoreDocument, + PublicUserDocument, + ScoreDocument, +} from "tachi-common"; const mockScorePB: PBScoreDocument<"usc:Controller" | "usc:Keyboard"> = { chartID: "USC_CHART_ID", @@ -53,7 +58,7 @@ t.test("#TachiScoreToServerScore", (t) => { t.beforeEach(ResetDBState); t.test("Should correctly convert a tachiScore to a serverScore", async (t) => { - await db.scores.insert(mockScoreDocument as ScoreDocument); + await db.scores.insert(mockScoreDocument); const res = await TachiScoreToServerScore(mockScorePB); @@ -235,7 +240,7 @@ const mockUserDocs = [2, 3, 4, 5, 6, 7, 8, 9, 10].map((e) => ({ id: e, username: e.toString(), usernameLowercase: e.toString(), -})) as PublicUserDocument[]; +})) as Array; t.test("#CreatePOSTScoresResponseBody", async (t) => { t.beforeEach(ResetDBState); @@ -257,11 +262,7 @@ t.test("#CreatePOSTScoresResponseBody", async (t) => { }, } as unknown as ScoreDocument); - const res = await CreatePOSTScoresResponseBody( - 1, - chartDoc as ChartDocument<"usc:Controller" | "usc:Keyboard">, - "USER_1_SCORE_PB" - ); + const res = await CreatePOSTScoresResponseBody(1, chartDoc, "USER_1_SCORE_PB"); t.hasStrict(res, { score: { diff --git a/server/src/utils/calculations/recalc-scores.ts b/server/src/utils/calculations/recalc-scores.ts index 83c400b61..3a5dac342 100644 --- a/server/src/utils/calculations/recalc-scores.ts +++ b/server/src/utils/calculations/recalc-scores.ts @@ -7,7 +7,6 @@ import { GetAndUpdateUsersGoals } from "lib/score-import/framework/goals/goals"; import { UpdateUsersMilestones } from "lib/score-import/framework/milestones/milestones"; import { ProcessPBs } from "lib/score-import/framework/pb/process-pbs"; import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/user-game-stats/update-ugs"; -import { Playtypes } from "tachi-common"; import { EfficientDBIterate } from "utils/efficient-db-iterate"; import { FormatUserDoc } from "utils/user"; import type { Game, integer, Playtype, PublicUserDocument } from "tachi-common"; diff --git a/server/src/utils/crypto.test.ts b/server/src/utils/crypto.test.ts index b318dc928..c9eff9edb 100644 --- a/server/src/utils/crypto.test.ts +++ b/server/src/utils/crypto.test.ts @@ -1,5 +1,5 @@ -import t from "tap"; import { HashSHA256 } from "./crypto"; +import t from "tap"; t.test("#HashSHA256", (t) => { t.equal( diff --git a/server/src/utils/db.test.ts b/server/src/utils/db.test.ts index 96f38414a..3656d8c26 100644 --- a/server/src/utils/db.test.ts +++ b/server/src/utils/db.test.ts @@ -1,7 +1,7 @@ +import { DecrementCounterValue, GetNextCounterValue } from "./db"; import db from "external/mongo/db"; import t from "tap"; import ResetDBState from "test-utils/resets"; -import { DecrementCounterValue, GetNextCounterValue } from "./db"; t.test("#GetNextCounterValue", (t) => { t.beforeEach(ResetDBState); diff --git a/server/src/utils/folder.test.ts b/server/src/utils/folder.test.ts index 54a954766..a1aa1e03b 100644 --- a/server/src/utils/folder.test.ts +++ b/server/src/utils/folder.test.ts @@ -1,10 +1,10 @@ +import { GetFolderChartIDs, ResolveFolderToCharts, TransposeFolderData } from "./folder"; import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import { ChartDocument, FolderDocument } from "tachi-common"; import t from "tap"; import ResetDBState from "test-utils/resets"; import { Testing511SPA } from "test-utils/test-data"; -import { GetFolderChartIDs, ResolveFolderToCharts, TransposeFolderData } from "./folder"; +import type { ChartDocument, FolderDocument } from "tachi-common"; t.todo("#CreateFolderChartLookup"); t.todo("#GetFolderCharts"); @@ -91,7 +91,8 @@ t.test("#ResolveFolderToCharts", (t) => { playtype: "SP", type: "charts", data: { - songID: Testing511SPA.songID, // lol + // lol + songID: Testing511SPA.songID, }, }; @@ -122,7 +123,8 @@ t.test("#ResolveFolderToCharts", (t) => { playtype: "SP", type: "charts", data: { - songID: Testing511SPA.songID, // lol + // lol + songID: Testing511SPA.songID, }, }; @@ -151,7 +153,8 @@ t.test("#ResolveFolderToCharts", (t) => { playtype: "SP", type: "charts", data: { - songID: Testing511SPA.songID, // lol + // lol + songID: Testing511SPA.songID, }, }; diff --git a/server/src/utils/misc.test.ts b/server/src/utils/misc.test.ts index a7108bf23..34552a9f8 100644 --- a/server/src/utils/misc.test.ts +++ b/server/src/utils/misc.test.ts @@ -1,8 +1,9 @@ -import t from "tap"; import { GetMillisecondsSince, IsValidURL, RoundToNDecimalPlaces } from "./misc"; +import t from "tap"; t.test("#GetMillisecondsSince", (t) => { const time = GetMillisecondsSince(10n); + t.ok(typeof time === "number" && time > 0, "Should return a number greater than 0."); t.end(); @@ -16,6 +17,7 @@ t.test("#IsValidURL", (t) => { t.ok(IsValidURL("http://example.com?querystring")); t.notOk(IsValidURL("ftp://example.com")); + // t.notOk(IsValidURL("http://example")); lol this is valid???? insane. // t.notOk(IsValidURL("http:/example.com")); this is also valid, the JS URL parser is ridiculously lenient. Whatever. diff --git a/server/src/utils/naive-csv-parser.test.ts b/server/src/utils/naive-csv-parser.test.ts index b54a4e975..7eea67b2d 100644 --- a/server/src/utils/naive-csv-parser.test.ts +++ b/server/src/utils/naive-csv-parser.test.ts @@ -1,7 +1,7 @@ +import { CSVParseError, NaiveCSVParse } from "./naive-csv-parser"; import CreateLogCtx from "lib/logger/logger"; import t from "tap"; import { TestingIIDXEamusementCSV27 } from "test-utils/test-data"; -import { CSVParseError, NaiveCSVParse } from "./naive-csv-parser"; const logger = CreateLogCtx(__filename); @@ -19,6 +19,7 @@ t.test("#ParseCSV", (t) => { const csvBuffer = Buffer.from(`${headersStr}\n${rowsStr}`); const { rawHeaders, rawRows } = NaiveCSVParse(csvBuffer, logger); + t.strictSame(rawHeaders, headers); t.strictSame(rawRows, rows); @@ -38,6 +39,7 @@ t.test("#ParseCSV", (t) => { const csvBuffer = Buffer.from(`${headersStr}\n${rowsStr}\n`); const { rawHeaders, rawRows } = NaiveCSVParse(csvBuffer, logger); + t.strictSame(rawHeaders, headers); t.strictSame(rawRows, rows); diff --git a/server/src/utils/prudence.ts b/server/src/utils/prudence.ts index 61c585197..213860a9d 100644 --- a/server/src/utils/prudence.ts +++ b/server/src/utils/prudence.ts @@ -5,9 +5,7 @@ export function FormatPrError(err: PrudenceError, foreword = "Error") { const receivedText = typeof err.userVal === "object" && err.userVal !== null ? "" - : ` | Received ${String(err.userVal)} [${ - err.userVal === null ? "null" : typeof err.userVal - }]`; + : ` | Received ${err.userVal} [${err.userVal === null ? "null" : typeof err.userVal}]`; return `${foreword}: ${err.keychain ?? "null"} | ${err.message}${receivedText}.`; } diff --git a/server/src/utils/session.test.ts b/server/src/utils/session.test.ts index 984d616ae..249bc74b7 100644 --- a/server/src/utils/session.test.ts +++ b/server/src/utils/session.test.ts @@ -1,7 +1,7 @@ +import { GetScoresFromSession } from "./session"; import db from "external/mongo/db"; import t from "tap"; import ResetDBState from "test-utils/resets"; -import { GetScoresFromSession } from "./session"; t.test("#GetScoresFromSession", async (t) => { t.beforeEach(ResetDBState); diff --git a/server/src/utils/user.test.ts b/server/src/utils/user.test.ts index 2986c774e..9d9921680 100644 --- a/server/src/utils/user.test.ts +++ b/server/src/utils/user.test.ts @@ -1,10 +1,10 @@ +import { FormatUserDoc, GetUserCaseInsensitive, GetUsersWithIDs } from "./user"; import db from "external/mongo/db"; import { DatabaseSchemas } from "external/mongo/schemas"; -import { PublicUserDocument } from "tachi-common"; import t from "tap"; import { mkFakeUser } from "test-utils/misc"; import ResetDBState from "test-utils/resets"; -import { FormatUserDoc, GetUserCaseInsensitive, GetUsersWithIDs } from "./user"; +import type { PublicUserDocument } from "tachi-common"; t.test("#GetUserCaseInsensitive", (t) => { t.beforeEach(ResetDBState); @@ -20,6 +20,7 @@ t.test("#GetUserCaseInsensitive", (t) => { // @ts-expect-error yeah t.equal(result.password, undefined, "Should not return password"); + // @ts-expect-error yeah t.equal(result.email, undefined, "Should not return email"); }); @@ -35,6 +36,7 @@ t.test("#GetUserCaseInsensitive", (t) => { // @ts-expect-error yeah t.equal(result.password, undefined, "Should not return password"); + // @ts-expect-error yeah t.equal(result.email, undefined, "Should not return email"); });