From 0adc39b5cbb2d26eacb1ce4c86d79b59b04b0aa3 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Fri, 6 May 2022 03:39:03 +0100 Subject: [PATCH] experimental: initial autofix blast --- server/@types/express/index.d.ts | 18 ++-- server/src/datasets/words.ts | 10 +++ server/src/external/mongo/db.ts | 16 ++-- server/src/external/mongo/indexes.ts | 14 +-- server/src/external/mongo/schemas.ts | 9 +- server/src/external/mongo/sequence-docs.ts | 2 +- server/src/external/redis/redis-IPC.ts | 6 +- .../lib/builtin-clients/builtin-clients.ts | 7 +- server/src/lib/cdn/cdn.ts | 10 +-- server/src/lib/cdn/s3.ts | 3 +- server/src/lib/cdn/url-format.ts | 2 +- server/src/lib/constants/tachi.ts | 4 +- server/src/lib/database-seeds/repo.ts | 29 +++--- server/src/lib/email/client.ts | 21 ++--- .../lib/game-settings/create-game-settings.ts | 2 +- server/src/lib/handlers/sigterm.ts | 4 +- server/src/lib/imports/imports.ts | 2 +- server/src/lib/invites/invites.ts | 4 +- server/src/lib/jobs/backsync-bms-pms-data.ts | 1 + server/src/lib/jobs/bms-table-sync.ts | 25 +++--- .../lib/jobs/inline-job-runner/job-runner.ts | 12 +-- server/src/lib/jobs/ugs-snapshot.ts | 5 +- server/src/lib/jobs/update-bpi-data.ts | 20 ++--- server/src/lib/jobs/update-dp-tiers.ts | 2 +- server/src/lib/logger/discord-transport.ts | 14 +-- server/src/lib/logger/logger.ts | 22 ++--- server/src/lib/migration/migrations.ts | 8 +- .../migrations/add-rivals-to-ugpt.ts | 2 +- .../notifications/notification-wrappers.ts | 5 +- server/src/lib/notifications/notifications.ts | 8 +- server/src/lib/orphan-queue/orphan-queue.ts | 12 +-- server/src/lib/rivals/rivals.ts | 5 +- .../calculated-data/calculated-data.ts | 38 ++++---- .../framework/calculated-data/stats.ts | 7 +- .../framework/common/converter-failures.ts | 4 +- .../framework/common/get-input-parser.ts | 10 +-- .../framework/common/import-logger.ts | 5 +- .../framework/common/score-utils.ts | 30 +++---- .../framework/common/string-asserts.ts | 4 +- .../score-import/framework/common/types.ts | 9 +- .../score-import/framework/express-wrapper.ts | 10 +-- .../lib/score-import/framework/goals/goals.ts | 19 ++-- .../framework/import-locks/lock.ts | 2 +- .../framework/milestones/milestones.ts | 23 ++--- .../score-import/framework/orphans/orphans.ts | 29 +++--- .../framework/pb/create-pb-doc.ts | 12 +-- .../framework/pb/game-specific-merge.ts | 25 +++--- .../score-import/framework/pb/process-pbs.ts | 9 +- .../score-import/framework/score-import.ts | 14 +-- .../score-importing/hydrate-score.ts | 23 ++--- .../framework/score-importing/insert-score.ts | 4 +- .../framework/score-importing/score-id.ts | 6 +- .../score-importing/score-import-main.ts | 53 +++++------ .../score-importing/score-importing.ts | 88 +++++++++---------- .../framework/sessions/calculated-data.ts | 19 ++-- .../framework/sessions/sessions.ts | 40 +++++---- .../user-game-stats/builtin-class-handlers.ts | 6 +- .../framework/user-game-stats/classes.ts | 37 ++++---- .../framework/user-game-stats/rating.ts | 30 ++++--- .../framework/user-game-stats/types.ts | 22 +++-- .../framework/user-game-stats/update-ugs.ts | 12 +-- .../api/arc-iidx/class-handler.ts | 4 +- .../import-types/api/arc-iidx/converter.ts | 24 ++--- .../import-types/api/arc-iidx/parser.ts | 13 +-- .../import-types/api/arc-iidx/types.ts | 12 +-- .../import-types/api/arc-sdvx/converter.ts | 19 ++-- .../import-types/api/arc-sdvx/parser.ts | 11 +-- .../import-types/api/arc-sdvx/types.ts | 4 +- .../import-types/api/eag-iidx/parser.ts | 6 +- .../import-types/api/eag-sdvx/parser.ts | 6 +- .../import-types/api/flo-iidx/parser.ts | 4 +- .../import-types/api/flo-sdvx/parser.ts | 6 +- .../import-types/api/min-sdvx/parser.ts | 6 +- .../common/api-kai/iidx/class-handler.ts | 8 +- .../common/api-kai/iidx/converter.ts | 66 +++++++++++--- .../common/api-kai/iidx/parser.ts | 14 +-- .../import-types/common/api-kai/reauth.ts | 10 ++- .../common/api-kai/sdvx/class-handler.ts | 8 +- .../common/api-kai/sdvx/converter.ts | 16 ++-- .../common/api-kai/sdvx/parser.ts | 14 +-- .../common/api-kai/traverse-api.ts | 12 +-- .../import-types/common/api-kai/types.ts | 8 +- .../import-types/common/api-kai/utils.ts | 7 +- .../common/batch-manual/class-handler.ts | 7 +- .../common/batch-manual/converter.ts | 55 ++++++------ .../common/batch-manual/parser.ts | 38 ++++---- .../import-types/common/batch-manual/types.ts | 2 +- .../common/eamusement-iidx-csv/converter.ts | 22 ++--- .../common/eamusement-iidx-csv/parser.ts | 20 +++-- .../common/eamusement-iidx-csv/types.ts | 29 +++--- .../score-import/import-types/common/types.ts | 66 +++++++------- .../score-import/import-types/converters.ts | 4 +- .../import-types/file/batch-manual/parser.ts | 10 +-- .../file/eamusement-iidx-csv/parser.ts | 6 +- .../file/eamusement-sdvx-csv/converter.ts | 11 +-- .../file/eamusement-sdvx-csv/parser.ts | 18 ++-- .../import-types/file/mer-iidx/converter.ts | 16 ++-- .../import-types/file/mer-iidx/parser.ts | 12 +-- .../import-types/file/mer-iidx/types.ts | 16 ++-- .../import-types/file/pli-iidx-csv/parser.ts | 6 +- .../file/solid-state-squad/converter.ts | 35 ++++---- .../file/solid-state-squad/parser.ts | 17 ++-- .../file/solid-state-squad/types.ts | 8 +- .../import-types/ir/barbatos/converter.ts | 18 ++-- .../import-types/ir/barbatos/parser.ts | 10 +-- .../import-types/ir/barbatos/types.ts | 2 +- .../import-types/ir/beatoraja/converter.ts | 33 +++---- .../import-types/ir/beatoraja/parser.ts | 13 +-- .../import-types/ir/beatoraja/types.ts | 18 ++-- .../import-types/ir/direct-manual/parser.ts | 8 +- .../ir/fervidex-static/class-handler.ts | 4 +- .../ir/fervidex-static/converter.ts | 16 ++-- .../import-types/ir/fervidex-static/parser.ts | 21 +++-- .../import-types/ir/fervidex-static/types.ts | 6 +- .../import-types/ir/fervidex/converter.ts | 48 ++++++---- .../import-types/ir/fervidex/parser.ts | 14 +-- .../import-types/ir/fervidex/types.ts | 33 +++---- .../import-types/ir/kshook-sv6c/converter.ts | 10 +-- .../import-types/ir/kshook-sv6c/parser.ts | 9 +- .../import-types/ir/kshook-sv6c/types.ts | 24 ++--- .../import-types/ir/lr2hook/converter.ts | 8 +- .../import-types/ir/lr2hook/parser.ts | 13 ++- .../import-types/ir/lr2hook/types.ts | 10 +-- .../import-types/ir/usc/converter.ts | 28 +++--- .../import-types/ir/usc/parser.ts | 17 ++-- .../score-import/import-types/ir/usc/types.ts | 2 +- server/src/lib/score-import/worker/types.ts | 10 +-- server/src/lib/score-import/worker/worker.ts | 26 +++--- .../src/lib/score-mutation/delete-scores.ts | 4 +- server/src/lib/score-mutation/update-score.ts | 14 +-- server/src/lib/search/search.ts | 31 ++++--- server/src/lib/setup/config.ts | 44 ++++++---- server/src/lib/showcase/evaluator.ts | 13 ++- server/src/lib/showcase/get-related.ts | 4 +- server/src/lib/showcase/get-stats.ts | 6 +- server/src/lib/showcase/showcase.ts | 2 +- server/src/lib/targets/goal-utils.ts | 28 +++--- server/src/lib/targets/goals.ts | 37 +++++--- server/src/lib/targets/milestones.ts | 16 ++-- server/src/lib/webhooks/webhooks.ts | 4 +- server/src/main.ts | 18 ++-- server/src/scripts/clone-and-anonymise-db.ts | 4 +- server/src/scripts/email/send-test-email.ts | 1 + .../scripts/iidx-tierlist/get-sp12-data.ts | 24 +++-- .../src/scripts/quick-insert/quick-insert.ts | 6 +- server/src/scripts/set-indexes.ts | 2 +- .../state-sync/recalc-game-profiles.ts | 1 + server/src/scripts/state-sync/sync-state.ts | 2 +- server/src/scripts/sync-database.ts | 71 +++++++++------ server/src/scripts/validate-database.ts | 9 +- server/src/server/middleware/auth.test.ts | 8 +- server/src/server/middleware/auth.ts | 46 +++++----- server/src/server/middleware/multer-upload.ts | 6 +- .../server/middleware/prudence-validate.ts | 8 +- server/src/server/middleware/rate-limiter.ts | 7 +- .../src/server/middleware/request-logger.ts | 8 +- .../src/server/middleware/sanitise-qstring.ts | 2 +- server/src/server/middleware/type-require.ts | 8 +- .../src/server/middleware/update-last-seen.ts | 10 ++- .../src/server/router/api/v1/admin/router.ts | 32 ++++--- server/src/server/router/api/v1/auth/auth.ts | 5 +- .../src/server/router/api/v1/auth/router.ts | 30 ++++--- .../router/api/v1/clients/middleware.test.ts | 4 +- .../router/api/v1/clients/middleware.ts | 13 +-- .../server/router/api/v1/clients/router.ts | 23 +++-- .../_playtype/charts/_chartID/middleware.ts | 4 +- .../_game/_playtype/charts/_chartID/router.ts | 21 ++--- .../v1/games/_game/_playtype/charts/router.ts | 19 ++-- .../_game/_playtype/folders/middleware.ts | 4 +- .../games/_game/_playtype/folders/router.ts | 8 +- .../v1/games/_game/_playtype/middleware.ts | 11 +-- .../api/v1/games/_game/_playtype/router.ts | 30 +++---- .../_playtype/songs/_songID/middleware.ts | 8 +- .../_game/_playtype/songs/_songID/router.ts | 6 +- .../_game/_playtype/tables/middleware.ts | 10 +-- .../v1/games/_game/_playtype/tables/router.ts | 10 +-- .../_game/_playtype/targets/goals/router.ts | 20 ++--- .../targets/milestone-sets/router.ts | 9 +- .../_playtype/targets/milestones/router.ts | 11 +-- .../games/_game/_playtype/targets/router.ts | 6 +- .../router/api/v1/games/_game/middleware.ts | 4 +- .../router/api/v1/games/_game/router.ts | 10 +-- .../src/server/router/api/v1/games/router.ts | 2 +- .../src/server/router/api/v1/import/router.ts | 56 ++++++------ .../router/api/v1/imports/middleware.ts | 17 ++-- .../server/router/api/v1/imports/router.ts | 18 ++-- .../src/server/router/api/v1/oauth/router.ts | 1 + server/src/server/router/api/v1/router.ts | 4 +- .../api/v1/scores/_scoreID/middleware.ts | 17 ++-- .../router/api/v1/scores/_scoreID/router.ts | 20 ++--- .../src/server/router/api/v1/scores/router.ts | 2 +- .../api/v1/sessions/_sessionID/middleware.ts | 19 ++-- .../api/v1/sessions/_sessionID/router.ts | 16 ++-- .../server/router/api/v1/sessions/router.ts | 2 +- .../src/server/router/api/v1/status/router.ts | 14 +-- .../api/v1/users/_userID/api-tokens/router.ts | 15 ++-- .../api/v1/users/_userID/banner/router.ts | 15 ++-- .../_playtype/folders/_folderID/router.ts | 21 ++--- .../games/_game/_playtype/folders/router.ts | 4 +- .../games/_game/_playtype/middleware.ts | 8 +- .../games/_game/_playtype/rivals/router.ts | 7 +- .../_userID/games/_game/_playtype/router.ts | 42 +++++---- .../games/_game/_playtype/sessions/router.ts | 1 + .../games/_game/_playtype/settings/router.ts | 3 +- .../games/_game/_playtype/showcase/router.ts | 16 ++-- .../games/_game/_playtype/tables/router.ts | 7 +- .../_game/_playtype/targets/goals/router.ts | 19 ++-- .../_playtype/targets/milestones/router.ts | 19 ++-- .../games/_game/_playtype/targets/router.ts | 4 +- .../api/v1/users/_userID/imports/router.ts | 6 +- .../users/_userID/integrations/arc/router.ts | 8 +- .../_userID/integrations/fervidex/router.ts | 8 +- .../integrations/kai/_kaiType/middleware.ts | 4 +- .../integrations/kai/_kaiType/router.ts | 19 ++-- .../v1/users/_userID/integrations/router.ts | 4 +- .../api/v1/users/_userID/invites/router.ts | 14 +-- .../router/api/v1/users/_userID/middleware.ts | 29 +++--- .../v1/users/_userID/notifications/router.ts | 12 +-- .../router/api/v1/users/_userID/pfp/router.ts | 16 ++-- .../router/api/v1/users/_userID/router.ts | 61 ++++++++----- .../api/v1/users/_userID/settings/router.ts | 8 +- .../src/server/router/api/v1/users/router.ts | 5 +- .../src/server/router/ir/barbatos/router.ts | 4 +- server/src/server/router/ir/beatoraja/auth.ts | 4 +- .../charts/_chartSHA256/convert-scores.ts | 11 +-- .../beatoraja/charts/_chartSHA256/router.ts | 20 +++-- .../src/server/router/ir/beatoraja/router.ts | 26 +++--- .../server/router/ir/direct-manual/router.ts | 28 +++--- .../src/server/router/ir/fervidex/router.ts | 58 +++++++----- server/src/server/router/ir/kshook/router.ts | 2 +- .../server/router/ir/kshook/sv6c/router.ts | 21 +++-- server/src/server/router/ir/lr2hook/router.ts | 8 +- server/src/server/router/ir/router.ts | 16 ++-- .../server/router/ir/usc/_playtype/router.ts | 37 ++++---- .../server/router/ir/usc/_playtype/types.ts | 8 +- .../src/server/router/ir/usc/_playtype/usc.ts | 15 ++-- server/src/server/router/ir/usc/router.ts | 2 +- server/src/server/router/router.ts | 8 +- server/src/server/server.ts | 14 +-- server/src/test-utils/api-common.ts | 10 +-- server/src/test-utils/asserts.ts | 4 +- server/src/test-utils/cleanup.ts | 8 +- server/src/test-utils/fake-auth.ts | 6 +- server/src/test-utils/misc.ts | 57 +++++------- server/src/test-utils/mock-api.ts | 2 +- server/src/test-utils/mock-fetch.ts | 4 +- server/src/test-utils/resets.ts | 18 ++-- server/src/test-utils/server.ts | 2 +- server/src/test-utils/setup.ts | 2 +- server/src/test-utils/single-process-tap.ts | 6 +- server/src/test-utils/test-data.ts | 22 ++--- .../src/utils/calculations/recalc-scores.ts | 21 ++--- .../src/utils/calculations/recalc-sessions.ts | 2 + server/src/utils/class.ts | 32 +++---- server/src/utils/db.ts | 73 +++++++-------- server/src/utils/ea3id.ts | 2 +- server/src/utils/efficient-db-iterate.ts | 6 +- server/src/utils/fetch.ts | 3 +- server/src/utils/folder.ts | 34 ++++--- server/src/utils/misc.ts | 40 +++++---- server/src/utils/naive-csv-parser.ts | 4 +- server/src/utils/prudence.ts | 9 +- server/src/utils/queries/auth.ts | 8 +- server/src/utils/queries/charts.ts | 45 +++++----- server/src/utils/queries/pbs.ts | 6 +- server/src/utils/queries/songs.ts | 13 +-- server/src/utils/queries/stats.ts | 10 +-- server/src/utils/queries/summary.ts | 2 +- server/src/utils/req-tachi-data.ts | 26 +++--- server/src/utils/reset-state/destroy-ugpt.ts | 3 +- server/src/utils/scores.ts | 16 ++-- server/src/utils/session.ts | 4 +- server/src/utils/string-checks.ts | 3 +- server/src/utils/types.ts | 4 +- server/src/utils/user.ts | 19 ++-- 275 files changed, 2207 insertions(+), 1877 deletions(-) diff --git a/server/@types/express/index.d.ts b/server/@types/express/index.d.ts index 25d980514..70b77a118 100644 --- a/server/@types/express/index.d.ts +++ b/server/@types/express/index.d.ts @@ -1,12 +1,11 @@ -import { Session, SessionData } from "express-session"; -import { SYMBOL_TachiAPIAuth, SYMBOL_TachiData } from "../../src/lib/constants/tachi"; -import { TachiRequestData, TachiSessionData } from "../../src/utils/types"; -import { APITokenDocument } from "tachi-common"; - -export {}; +import type { SYMBOL_TACHI_API_AUTH, SYMBOL_TACHI_DATA } from "../../src/lib/constants/tachi"; +import type { TachiRequestData, TachiSessionData } from "../../src/utils/types"; +import type { Session, SessionData } from "express-session"; +import type { APITokenDocument } from "tachi-common"; // this only exists for testing context - it doesn't seem to work properly otherwise. declare module "express-session" { + // Inject additional properties on express-session interface SessionData { tachi: TachiSessionData; @@ -16,12 +15,13 @@ declare module "express-session" { declare global { namespace Express { interface Request { - session: Session & Partial; + session: Partial & Session; + + [SYMBOL_TACHI_DATA]?: Partial; - [SYMBOL_TachiData]?: Partial; // even though this is technically *not* present on every request // it's always assigned in the main router, so its functionally equivalent. - [SYMBOL_TachiAPIAuth]: APITokenDocument; + [SYMBOL_TACHI_API_AUTH]: APITokenDocument; } } } diff --git a/server/src/datasets/words.ts b/server/src/datasets/words.ts index 78b213ea3..8aaafb7d8 100644 --- a/server/src/datasets/words.ts +++ b/server/src/datasets/words.ts @@ -106,6 +106,7 @@ export const adjectives = [ "Biodegradable", "Bite-sized", "Bitter", + // "Black", - Could result in racism. "Black-and-white", "Bland", @@ -135,6 +136,7 @@ export const adjectives = [ "Brisk", "Broken", "Bronze", + // "Brown", - Could result in racism "Bruised", "Bubbly", @@ -702,6 +704,7 @@ export const adjectives = [ "Magnificent", "Majestic", "Major", + // "Male", - try and avoid sexism "Mammoth", "Married", @@ -1308,6 +1311,7 @@ export const adjectives = [ "Whimsical", "Whirlwind", "Whispered", + // "White", - Could result in racism "Whole", "Whopping", @@ -2027,6 +2031,7 @@ export const nouns = [ "Share", "Balance", "Bit", + // "Black", I have visions of this one resulting in racism "Bottom", // This one gets to stay because it's hilarious "Choice", @@ -2136,6 +2141,7 @@ export const nouns = [ "Park", "Plenty", "Quarter", + // "Skin", - Could possibly be racist. "Sort", "Weight", @@ -2236,6 +2242,7 @@ export const nouns = [ "Pleasure", "Score", "Screw", + // "Sex", - Absolutely not, Thanks for this being in a SFW wordlist... "Shop", "Shower", @@ -2333,6 +2340,7 @@ export const nouns = [ "Aside", "Associate", "Bet", + // "Blow", - maybe not. "Border", "Branch", @@ -2609,6 +2617,7 @@ export const nouns = [ "Report", "Rise", "Walk", + // "White", - Could result in racism. "Beyond", "Junior", @@ -2830,6 +2839,7 @@ export const nouns = [ "Dear", "Delay", "Drunk", + // "Female", try and avoid potential sexism "Hurry", "Inevitable", diff --git a/server/src/external/mongo/db.ts b/server/src/external/mongo/db.ts index 5f066a374..7c359856c 100644 --- a/server/src/external/mongo/db.ts +++ b/server/src/external/mongo/db.ts @@ -1,9 +1,11 @@ import { ONE_MINUTE } from "lib/constants/time"; import CreateLogCtx from "lib/logger/logger"; -import { OrphanScoreDocument } from "lib/score-import/import-types/common/types"; import { Environment, ServerConfig } from "lib/setup/config"; -import monk, { TMiddleware } from "monk"; -import { +import monk from "monk"; +import { GetMillisecondsSince } from "utils/misc"; +import type { OrphanScoreDocument } from "lib/score-import/import-types/common/types"; +import type { TMiddleware } from "monk"; +import type { APITokenDocument, ARCSavedProfileDocument, BMSCourseDocument, @@ -40,8 +42,8 @@ import { UserGameStatsSnapshot, UserSettings, } from "tachi-common"; -import { GetMillisecondsSince } from "utils/misc"; -import { MigrationDocument, PrivateUserInfoDocument } from "utils/types"; +import type { MigrationDocument, PrivateUserInfoDocument } from "utils/types"; + const logger = CreateLogCtx(__filename); let dbName = ServerConfig.MONGO_DATABASE_NAME; @@ -197,8 +199,8 @@ const db = { notifications: monkDB.get("notifications"), }; -export type StaticDatabases = Exclude; +export type StaticDatabases = Exclude; -export type Databases = StaticDatabases | `songs-${Game}` | `charts-${Game}`; +export type Databases = StaticDatabases | `charts-${Game}` | `songs-${Game}`; export default db; diff --git a/server/src/external/mongo/indexes.ts b/server/src/external/mongo/indexes.ts index 2083517a6..7fe729520 100644 --- a/server/src/external/mongo/indexes.ts +++ b/server/src/external/mongo/indexes.ts @@ -1,11 +1,13 @@ /* eslint-disable no-await-in-loop */ +import db, { monkDB } from "./db"; import { ONE_DAY } from "lib/constants/time"; import CreateLogCtx from "lib/logger/logger"; import { TachiConfig } from "lib/setup/config"; -import { IndexOptions } from "mongodb"; -import monk, { IMonkManager } from "monk"; +import monk from "monk"; import { Random20Hex } from "utils/misc"; -import db, { Databases, monkDB } from "./db"; +import type { Databases } from "./db"; +import type { IndexOptions } from "mongodb"; +import type { IMonkManager } from "monk"; const logger = CreateLogCtx(__filename); @@ -20,7 +22,7 @@ function index(fields: Record, options?: IndexOptions) { const UNIQUE = { unique: true }; -const staticIndexes: Partial> = { +const staticIndexes: Partial>> = { scores: [ index({ scoreID: 1 }, UNIQUE), index({ chartID: 1, userID: 1 }), @@ -33,6 +35,7 @@ const staticIndexes: Partial> = { ], sessions: [ index({ userID: 1, game: 1, playtype: 1, timeStarted: 1, timeEnded: 1 }), + // Optimises score modification, since sessions need to be repointed. // also, just generally useful. index({ "scoreInfo.scoreID": 1 }), @@ -122,7 +125,7 @@ const staticIndexes: Partial> = { notifications: [index({ notifID: 1 }, UNIQUE), index({ sentTo: 1, sentAt: 1 })], }; -const indexes: Partial> = staticIndexes; +const indexes: Partial>> = staticIndexes; for (const game of TachiConfig.GAMES) { if (indexes[`charts-${game}` as Databases]) { @@ -166,6 +169,7 @@ export async function SetIndexesWithDB(db: IMonkManager, reset: boolean) { // this creates a collection, i cant find the createCollection // call. const tmp = Random20Hex(); + await db.get(collection).insert({ __tmp: tmp }); await db.get(collection).remove({ __tmp: tmp }); } diff --git a/server/src/external/mongo/schemas.ts b/server/src/external/mongo/schemas.ts index 8ed71ed51..40f592a47 100644 --- a/server/src/external/mongo/schemas.ts +++ b/server/src/external/mongo/schemas.ts @@ -1,7 +1,9 @@ import { TachiConfig } from "lib/setup/config"; -import p, { PrudenceSchema } from "prudence"; -import { SCHEMAS, SchemaValidatorFunction } from "tachi-common/js/lib/schemas"; -import { Databases } from "./db"; +import p from "prudence"; +import { SCHEMAS } from "tachi-common/js/lib/schemas"; +import type { Databases } from "./db"; +import type { PrudenceSchema } from "prudence"; +import type { SchemaValidatorFunction } from "tachi-common/js/lib/schemas"; // The idea of this file is to export a function for every database // that will validate that collection. @@ -67,6 +69,7 @@ export const DatabaseSchemas: Record = { orphanID: "string", timeInserted: p.isPositive, errMsg: "?string", + // @todo Type these properly? This will take a lot of effort, as it will require // formal types for all parser data:context duos. data: p.any, diff --git a/server/src/external/mongo/sequence-docs.ts b/server/src/external/mongo/sequence-docs.ts index 2b50d07c0..44e44c9d8 100644 --- a/server/src/external/mongo/sequence-docs.ts +++ b/server/src/external/mongo/sequence-docs.ts @@ -1,5 +1,5 @@ -import CreateLogCtx from "lib/logger/logger"; import db from "./db"; +import CreateLogCtx from "lib/logger/logger"; const logger = CreateLogCtx(__filename); diff --git a/server/src/external/redis/redis-IPC.ts b/server/src/external/redis/redis-IPC.ts index d048dd2e9..ac752cbe2 100644 --- a/server/src/external/redis/redis-IPC.ts +++ b/server/src/external/redis/redis-IPC.ts @@ -15,14 +15,14 @@ const logger = CreateLogCtx(__filename); // Stub types. In the future, we may actually use this. export type RedisIPCChannels = ""; -export type RedisIPCData = { +export interface RedisIPCData { "": unknown; -}; +} type RedisSubCallback = (data: RedisIPCData[T]) => void; type SubCallbacks = { - [K in RedisIPCChannels]: RedisSubCallback[]; + [K in RedisIPCChannels]: Array>; }; const SubCallbacks: Partial = {}; diff --git a/server/src/lib/builtin-clients/builtin-clients.ts b/server/src/lib/builtin-clients/builtin-clients.ts index 88a56db7f..5ce37090e 100644 --- a/server/src/lib/builtin-clients/builtin-clients.ts +++ b/server/src/lib/builtin-clients/builtin-clients.ts @@ -4,14 +4,15 @@ import { DatabaseSchemas } from "external/mongo/schemas"; import fjsh from "fast-json-stable-hash"; import CreateLogCtx from "lib/logger/logger"; import { ServerConfig, TachiConfig } from "lib/setup/config"; -import { PrudenceError } from "prudence"; -import { TachiAPIClientDocument, UserAuthLevels } from "tachi-common"; +import { UserAuthLevels } from "tachi-common"; import { Random20Hex } from "utils/misc"; import { FormatPrError } from "utils/prudence"; +import type { PrudenceError } from "prudence"; +import type { TachiAPIClientDocument } from "tachi-common"; const logger = CreateLogCtx(__filename); -type DefaultClients = Omit[]; +type DefaultClients = Array>; // Defines some Tachi API Clients that should come default with a Tachi // environment. diff --git a/server/src/lib/cdn/cdn.ts b/server/src/lib/cdn/cdn.ts index 0326b1d24..7becf49fe 100644 --- a/server/src/lib/cdn/cdn.ts +++ b/server/src/lib/cdn/cdn.ts @@ -1,11 +1,11 @@ -import { Response } from "express"; -import fs from "fs"; +import { DeleteFromS3, PushToS3 } from "./s3"; import CreateLogCtx from "lib/logger/logger"; import { ServerConfig } from "lib/setup/config"; import mkdirp from "mkdirp"; +import fs from "fs"; import path from "path"; import { promisify } from "util"; -import { DeleteFromS3, PushToS3 } from "./s3"; +import type { Response } from "express"; const readFilePromise = promisify(fs.readFile); const writeFilePromise = promisify(fs.writeFile); @@ -50,13 +50,13 @@ export function CDNRetrieve(fileLoc: string) { * Redirects the response to the CDN server at the given path. */ export function CDNRedirect(res: Response, fileLoc: string) { - if (fileLoc[0] !== "/") { + if (!fileLoc.startsWith("/")) { throw new Error(`Invalid fileLoc - did not start with /.`); } logger.debug(`CDN Redirecting to ${ServerConfig.CDN_CONFIG.WEB_LOCATION}${fileLoc}.`); - return res.redirect(`${ServerConfig.CDN_CONFIG.WEB_LOCATION}${fileLoc}`); + res.redirect(`${ServerConfig.CDN_CONFIG.WEB_LOCATION}${fileLoc}`); } /** diff --git a/server/src/lib/cdn/s3.ts b/server/src/lib/cdn/s3.ts index 394047b0e..5552239ac 100644 --- a/server/src/lib/cdn/s3.ts +++ b/server/src/lib/cdn/s3.ts @@ -4,10 +4,11 @@ import { ServerConfig } from "lib/setup/config"; const logger = CreateLogCtx("S3 Client"); -let s3: null | S3Client = null; +let s3: S3Client | null = null; if (ServerConfig.CDN_CONFIG.SAVE_LOCATION.TYPE === "S3_BUCKET") { const data = ServerConfig.CDN_CONFIG.SAVE_LOCATION; + logger.info(`Using S3_BUCKET as CDN location.`, { bootInfo: true }); s3 = new S3Client({ endpoint: data.ENDPOINT, diff --git a/server/src/lib/cdn/url-format.ts b/server/src/lib/cdn/url-format.ts index 3303b5f94..a7a1d133e 100644 --- a/server/src/lib/cdn/url-format.ts +++ b/server/src/lib/cdn/url-format.ts @@ -1,4 +1,4 @@ -import { integer } from "tachi-common"; +import type { integer } from "tachi-common"; export function GetUSCIRReplayURL(scoreID: string) { return `/uscir/replays/${scoreID}`; diff --git a/server/src/lib/constants/tachi.ts b/server/src/lib/constants/tachi.ts index c7247bf91..893e99c1e 100644 --- a/server/src/lib/constants/tachi.ts +++ b/server/src/lib/constants/tachi.ts @@ -1,3 +1,3 @@ -export const SYMBOL_TachiData = Symbol("tachi-request-patch"); -export const SYMBOL_TachiAPIAuth = Symbol("tachi-api-auth-info"); +export const SYMBOL_TACHI_DATA = Symbol("tachi-request-patch"); +export const SYMBOL_TACHI_API_AUTH = Symbol("tachi-api-auth-info"); export const JOB_RETRY_COUNT = 7; diff --git a/server/src/lib/database-seeds/repo.ts b/server/src/lib/database-seeds/repo.ts index a5384bc15..fc31d0fda 100644 --- a/server/src/lib/database-seeds/repo.ts +++ b/server/src/lib/database-seeds/repo.ts @@ -1,26 +1,26 @@ -import fs from "fs/promises"; import CreateLogCtx from "lib/logger/logger"; import { Environment, ServerConfig } from "lib/setup/config"; +import { asyncExec } from "utils/misc"; +import fs from "fs/promises"; import os from "os"; import path from "path"; -import { Game } from "tachi-common"; -import { asyncExec } from "utils/misc"; +import type { Game } from "tachi-common"; const logger = CreateLogCtx(__filename); export type SeedsCollections = - | `charts-${Game}` - | `songs-${Game}` | "bms-course-lookup" | "folders" - | "tables"; + | "tables" + | `charts-${Game}` + | `songs-${Game}`; /** * Class that encapsulates the behaviour of a seeds repo. */ export class DatabaseSeedsRepo { - private baseDir: string; - private logger; + private readonly baseDir: string; + private readonly logger; constructor(baseDir: string) { this.baseDir = baseDir; @@ -36,12 +36,12 @@ export class DatabaseSeedsRepo { * * @returns The data in the requested collection. */ - async ReadCollection(collectionName: SeedsCollections): Promise { + async ReadCollection(collectionName: SeedsCollections): Promise> { const data = await fs.readFile(this.CollectionNameToPath(collectionName), { encoding: "utf-8", }); - const parsedData = JSON.parse(data) as D[]; + const parsedData = JSON.parse(data) as Array; return parsedData; } @@ -52,7 +52,7 @@ export class DatabaseSeedsRepo { * @param collectionName - The collection to write to. * @param content - A new array of objects to write. */ - async WriteCollection(collectionName: SeedsCollections, content: unknown[]) { + async WriteCollection(collectionName: SeedsCollections, content: Array) { await fs.writeFile(this.CollectionNameToPath(collectionName), JSON.stringify(content)); // Deterministically sort whatever content we just wrote. @@ -64,7 +64,7 @@ export class DatabaseSeedsRepo { async *IterateCollections() { const collectionNames = (await fs.readdir(path.join(this.baseDir, "collections"))).map( (e) => path.parse(e).name - ) as SeedsCollections[]; + ) as Array; for (const collectionName of collectionNames) { // eslint-disable-next-line no-await-in-loop @@ -78,7 +78,10 @@ export class DatabaseSeedsRepo { * @param collectionName - The collection to mutate. * @param mutator - A function that takes the entire collection as an array, then returns a new array. */ - async MutateCollection(collectionName: SeedsCollections, mutator: (dataset: D[]) => D[]) { + async MutateCollection( + collectionName: SeedsCollections, + mutator: (dataset: Array) => Array + ) { const dataset = await this.ReadCollection(collectionName); const newData = mutator(dataset); diff --git a/server/src/lib/email/client.ts b/server/src/lib/email/client.ts index 71d4e65e7..d8e0c6c44 100644 --- a/server/src/lib/email/client.ts +++ b/server/src/lib/email/client.ts @@ -1,7 +1,8 @@ import bunyan from "bunyan"; import CreateLogCtx from "lib/logger/logger"; import { Environment, ServerConfig } from "lib/setup/config"; -import nodemailer, { SentMessageInfo, Transporter } from "nodemailer"; +import nodemailer from "nodemailer"; +import type { SentMessageInfo, Transporter } from "nodemailer"; const logger = CreateLogCtx(__filename); @@ -12,17 +13,13 @@ if (ServerConfig.EMAIL_CONFIG) { const conf = ServerConfig.EMAIL_CONFIG; try { - transporter = nodemailer.createTransport( - Object.assign( - { - newline: "unix", - logger: conf.TRANSPORT_OPS?.debug - ? bunyan.createLogger({ name: "Email Logger" }) - : undefined, - }, - conf.TRANSPORT_OPS ?? {} - ) - ); + transporter = nodemailer.createTransport({ + newline: "unix", + logger: conf.TRANSPORT_OPS?.debug + ? bunyan.createLogger({ name: "Email Logger" }) + : undefined, + ...(conf.TRANSPORT_OPS ?? {}), + }); transporter.verify((err) => { if (err) { diff --git a/server/src/lib/game-settings/create-game-settings.ts b/server/src/lib/game-settings/create-game-settings.ts index 035b628d1..35f030966 100644 --- a/server/src/lib/game-settings/create-game-settings.ts +++ b/server/src/lib/game-settings/create-game-settings.ts @@ -1,6 +1,6 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; -import { Game, integer, Playtype } from "tachi-common"; +import type { Game, integer, Playtype } from "tachi-common"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/handlers/sigterm.ts b/server/src/lib/handlers/sigterm.ts index 84b3f235a..2b5947fbb 100644 --- a/server/src/lib/handlers/sigterm.ts +++ b/server/src/lib/handlers/sigterm.ts @@ -1,9 +1,9 @@ import { monkDB } from "external/mongo/db"; import { CloseRedisConnection } from "external/redis/redis"; -import http from "http"; -import https from "https"; import { rootLogger } from "lib/logger/logger"; import { CloseScoreImportQueue } from "lib/score-import/worker/queue"; +import type http from "http"; +import type https from "https"; const logger = rootLogger; diff --git a/server/src/lib/imports/imports.ts b/server/src/lib/imports/imports.ts index 2c976007d..30b66bf58 100644 --- a/server/src/lib/imports/imports.ts +++ b/server/src/lib/imports/imports.ts @@ -1,7 +1,7 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; import { DeleteMultipleScores } from "lib/score-mutation/delete-scores"; -import { ImportDocument } from "tachi-common"; +import type { ImportDocument } from "tachi-common"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/invites/invites.ts b/server/src/lib/invites/invites.ts index 06e7c0b75..6d3e6c85d 100644 --- a/server/src/lib/invites/invites.ts +++ b/server/src/lib/invites/invites.ts @@ -1,7 +1,7 @@ import { ONE_MONTH } from "lib/constants/time"; import CreateLogCtx from "lib/logger/logger"; import { ServerConfig } from "lib/setup/config"; -import { PublicUserDocument } from "tachi-common"; +import type { PublicUserDocument } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -27,7 +27,7 @@ export function GetTotalAllowedInvites(user: PublicUserDocument) { let invites = monthsSinceJoin * ServerConfig.INVITE_CODE_CONFIG.BATCH_SIZE; if (user.badges.includes("alpha") || user.badges.includes("beta")) { - invites += ServerConfig.INVITE_CODE_CONFIG.BETA_USER_BONUS; + invites = invites + ServerConfig.INVITE_CODE_CONFIG.BETA_USER_BONUS; } if (invites > ServerConfig.INVITE_CODE_CONFIG.INVITE_CAP) { diff --git a/server/src/lib/jobs/backsync-bms-pms-data.ts b/server/src/lib/jobs/backsync-bms-pms-data.ts index 7eba57978..c85db8f3e 100644 --- a/server/src/lib/jobs/backsync-bms-pms-data.ts +++ b/server/src/lib/jobs/backsync-bms-pms-data.ts @@ -14,6 +14,7 @@ export async function BacksyncBMSPMSSongsAndCharts() { for (const game of ["bms", "pms"] as const) { logger.info(`Fetching BMS songs from DB.`); + // did you know, this code is liable to blow up in my face and OOM one day? let songs = await db.songs[game].find({}); diff --git a/server/src/lib/jobs/bms-table-sync.ts b/server/src/lib/jobs/bms-table-sync.ts index 5ee519f04..6ed940e8f 100644 --- a/server/src/lib/jobs/bms-table-sync.ts +++ b/server/src/lib/jobs/bms-table-sync.ts @@ -1,9 +1,17 @@ +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"; +import { FormatBMSTables } from "utils/misc"; +import type { ChartDocument } from "tachi-common"; + // I have no confidence in half of these links surviving. // Eventually, some of these are going to disappear, and we'll have to find // something else. probably. -const registeredTables: BMSTablesDataset[] = [ +const registeredTables: Array = [ { name: "Insane", humanisedPrefix: "insane", @@ -102,14 +110,6 @@ const registeredTables: BMSTablesDataset[] = [ }, ]; -/* eslint-disable no-await-in-loop */ -import db from "external/mongo/db"; -import CreateLogCtx from "lib/logger/logger"; -import fetch from "node-fetch"; -import { ChartDocument } from "tachi-common"; -import { CreateFolderID } from "utils/folder"; -import { FormatBMSTables } from "utils/misc"; - // this seems to be all we care about interface TableJSONDoc { md5: string; @@ -119,7 +119,7 @@ interface TableJSONDoc { const logger = CreateLogCtx(__filename); -async function ImportTableLevels(tableJSON: TableJSONDoc[], prefix: string) { +async function ImportTableLevels(tableJSON: Array, prefix: string) { let failures = 0; let success = 0; const total = tableJSON.length; @@ -201,7 +201,7 @@ export async function UpdateTable(table: BMSTablesDataset) { // instead of using res.json() we have to use res.text() here // so we have proper logging when some of these bms urls // spontaneously return HTML. - let tableJSON: BMSTableChart[] | undefined; + let tableJSON: Array | undefined; // We need to have a retry counter because some of the bms tables // randomly return useless html for no reason. @@ -209,6 +209,7 @@ export async function UpdateTable(table: BMSTablesDataset) { // i hate bms. while (!tableJSON) { let text; + try { text = await res.text(); tableJSON = JSON.parse(text); @@ -220,6 +221,7 @@ export async function UpdateTable(table: BMSTablesDataset) { logger.error(`SKIPPING TABLE!`); return; } + logger.info(`Retrying ${table.url}.`); } } @@ -234,6 +236,7 @@ export async function UpdateTable(table: BMSTablesDataset) { "data¬tableFolders": { "~elemMatch": { table: table.prefix, + // just incase some dude tries numbers level: level.toString(), }, 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 8841a0697..93d3e8e53 100644 --- a/server/src/lib/jobs/inline-job-runner/job-runner.ts +++ b/server/src/lib/jobs/inline-job-runner/job-runner.ts @@ -1,12 +1,12 @@ -import { Queue, Worker } from "bullmq"; -import CreateLogCtx from "lib/logger/logger"; -import { TachiConfig } from "lib/setup/config"; -import { DedupeArr } from "utils/misc"; import { BacksyncBMSPMSSongsAndCharts } from "../backsync-bms-pms-data"; import { DeoprhanScores } from "../deorphan-scores"; import { UGSSnapshot } from "../ugs-snapshot"; import { UpdatePoyashiData } from "../update-bpi-data"; import { UpdateDPTiers } from "../update-dp-tiers"; +import { Queue, Worker } from "bullmq"; +import CreateLogCtx from "lib/logger/logger"; +import { TachiConfig } from "lib/setup/config"; +import { DedupeArr } from "utils/misc"; interface Job { name: string; @@ -14,7 +14,7 @@ interface Job { run: () => Promise; } -const jobs: Job[] = [ +const jobs: Array = [ { name: "Snapshot User Game Stats", cronFormat: "0 0 * * *", @@ -22,6 +22,7 @@ const jobs: Job[] = [ }, { name: "De-Orphan Scores", + // We run an hour after snapshotting UGS // just to spread load out a bit. cronFormat: "1 0 * * *", @@ -79,6 +80,7 @@ export function InitialiseJobRunner() { new Worker("Job Runner", async (j) => { const name = j.data.jobName; + logger.info(`Running job ${name}.`); const jobInfo = jobNameMap.get(name); diff --git a/server/src/lib/jobs/ugs-snapshot.ts b/server/src/lib/jobs/ugs-snapshot.ts index c5a509078..2fcf4817b 100644 --- a/server/src/lib/jobs/ugs-snapshot.ts +++ b/server/src/lib/jobs/ugs-snapshot.ts @@ -1,8 +1,8 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; -import { UserGameStats, UserGameStatsSnapshot } from "tachi-common"; import { GetMillisecondsSince } from "utils/misc"; import { GetAllRankings } from "utils/user"; +import type { UserGameStats, UserGameStatsSnapshot } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -10,7 +10,7 @@ const logger = CreateLogCtx(__filename); // nonsense happens. we'll have to see. const currentTime = new Date().setUTCHours(0, 0, 0, 0); -let batchWrite: UserGameStatsSnapshot[] = []; +let batchWrite: Array = []; // This code is intentionally *very* robust, and handles a lot of unanticipated failures // because if it breaks, we brick the database. @@ -34,6 +34,7 @@ export async function UGSSnapshot() { try { await db["game-stats"] .find({}) + // @ts-expect-error faulty TS types .each(async (ugs: UserGameStats, { pause, resume }) => { pause(); diff --git a/server/src/lib/jobs/update-bpi-data.ts b/server/src/lib/jobs/update-bpi-data.ts index 4352e19dc..e326f4af9 100644 --- a/server/src/lib/jobs/update-bpi-data.ts +++ b/server/src/lib/jobs/update-bpi-data.ts @@ -1,6 +1,8 @@ import { PullDatabaseSeeds } from "lib/database-seeds/repo"; import CreateLogCtx from "lib/logger/logger"; -import { +import { RecalcAllScores } from "utils/calculations/recalc-scores"; +import fetch from "utils/fetch"; +import type { ChartDocument, Difficulties, GPTSupportedVersions, @@ -8,8 +10,6 @@ import { Playtypes, SongDocument, } from "tachi-common"; -import { RecalcAllScores } from "utils/calculations/recalc-scores"; -import fetch from "utils/fetch"; const logger = CreateLogCtx(__filename); @@ -20,7 +20,7 @@ const difficultyResolve = { 9: ["DP", "ANOTHER"], 10: ["SP", "LEGGENDARIA"], 11: ["DP", "LEGGENDARIA"], -} as Record; +} as Record; interface PoyashiProxyBPIInfo { title: string; @@ -39,7 +39,7 @@ interface PoyashiProxyBPIInfo { interface PoyashiProxyData { version: integer; requireVersion: string; - body: PoyashiProxyBPIInfo[]; + body: Array; } /** @@ -59,10 +59,8 @@ export async function UpdatePoyashiData() { logger.info("Fetched data."); - const iidxSongs = (await repo.ReadCollection("songs-iidx")) as SongDocument<"iidx">[]; - const iidxCharts = (await repo.ReadCollection("charts-iidx")) as ChartDocument< - "iidx:SP" | "iidx:DP" - >[]; + const iidxSongs = await repo.ReadCollection("songs-iidx"); + const iidxCharts = await repo.ReadCollection("charts-iidx"); // Utility functions for finding matching charts. function FindSongOnTitle(title: string) { @@ -78,8 +76,8 @@ export async function UpdatePoyashiData() { function FindChartWithPTDFVersion( songID: integer, playtype: Playtypes["iidx"], - diff: Difficulties["iidx:SP" | "iidx:DP"], - version: GPTSupportedVersions["iidx:SP" | "iidx:DP"] + diff: Difficulties["iidx:DP" | "iidx:SP"], + version: GPTSupportedVersions["iidx:DP" | "iidx:SP"] ) { for (const chart of iidxCharts) { if ( diff --git a/server/src/lib/jobs/update-dp-tiers.ts b/server/src/lib/jobs/update-dp-tiers.ts index 125c38d20..029d8a514 100644 --- a/server/src/lib/jobs/update-dp-tiers.ts +++ b/server/src/lib/jobs/update-dp-tiers.ts @@ -117,7 +117,7 @@ function ParseTierStr(tierStr: string) { return null; } - const result = tierStr.match(/\((.*)\)$/u); + const result = /\((.*)\)$/u.exec(tierStr); if (result && result[1]) { return Number(result[1]); diff --git a/server/src/lib/logger/discord-transport.ts b/server/src/lib/logger/discord-transport.ts index c8972983f..c9cd5dd8e 100644 --- a/server/src/lib/logger/discord-transport.ts +++ b/server/src/lib/logger/discord-transport.ts @@ -1,10 +1,11 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { DiscordColours } from "./colours"; 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 Transport, { TransportStreamOptions } from "winston-transport"; -import { DiscordColours } from "./colours"; +import Transport from "winston-transport"; +import type { TransportStreamOptions } from "winston-transport"; interface DiscordTransportOptions extends TransportStreamOptions { /** Webhook obtained from Discord */ @@ -12,8 +13,8 @@ interface DiscordTransportOptions extends TransportStreamOptions { } interface LogLevelCountState { - warn: string[]; - error: string[]; + warn: Array; + error: Array; } /** @@ -27,7 +28,7 @@ export default class DiscordTransport extends Transport { private webhookUrl = ""; /** Initialization promise resolved after retrieving discord id and token */ - private initalised: Promise; + private readonly initalised: Promise; private bucketData: LogLevelCountState = { warn: [], @@ -90,7 +91,7 @@ export default class DiscordTransport extends Transport { return; } - this.bucketData[info.level as "warn" | "error"].push(info.message); + this.bucketData[info.level as "error" | "warn"].push(info.message); if (!this.isBucketing) { this.isBucketing = true; @@ -158,6 +159,7 @@ export default class DiscordTransport extends Transport { embeds: [ { description: `[${info.level}] ${info.message}`, + // it's Colour!! color: DiscordColours[info.level as keyof typeof DiscordColours], timestamp: new Date().toISOString(), diff --git a/server/src/lib/logger/logger.ts b/server/src/lib/logger/logger.ts index 4960230d3..bb29dd236 100644 --- a/server/src/lib/logger/logger.ts +++ b/server/src/lib/logger/logger.ts @@ -1,9 +1,10 @@ import { Transport as SeqTransport } from "@valuabletouch/winston-seq"; import { Environment, ServerConfig, TachiConfig } from "lib/setup/config"; import SafeJSONStringify from "safe-json-stringify"; -import { SeqLogLevel } from "seq-logging"; import { EscapeStringRegexp } from "utils/misc"; -import winston, { format, LeveledLogMethod, Logger, transports } from "winston"; +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"; @@ -13,12 +14,14 @@ 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]; if (val instanceof Error) { meta[key] = { message: val.message, stack: val.stack }; } + i++; } @@ -41,7 +44,7 @@ function StrCap(string: string) { const formatExcessPropertiesNoStack = ( meta: Record, - omitKeys: string[] = [], + omitKeys: Array = [], limit = false ) => { const realMeta: Record = {}; @@ -116,7 +119,7 @@ const consoleFormatRoute = format.combine( }) ); -const tports: winston.transport[] = []; +const tports: Array = []; if (ServerConfig.LOGGER_CONFIG.FILE) { tports.push( @@ -159,6 +162,7 @@ if (ServerConfig.LOGGER_CONFIG.SEQ_API_KEY && Environment.seqUrl) { error: "Error", warn: "Warning", info: "Information", + // Note that Seq interprets these in reverse, // however, it's easier to read this code if I just // use the same levels, instead of the right ones. @@ -196,12 +200,12 @@ export const rootLogger = winston.createLogger({ transports: tports, defaultMeta: { __ServerName: TachiConfig.NAME, - __Worker: !!process.env.IS_WORKER, + __Worker: Boolean(process.env.IS_WORKER), __ReplicaID: Environment.replicaIdentity, }, }) as KtLogger; -if (!!ServerConfig.LOGGER_CONFIG.SEQ_API_KEY !== !!Environment.seqUrl) { +if (Boolean(ServerConfig.LOGGER_CONFIG.SEQ_API_KEY) !== Boolean(Environment.seqUrl)) { rootLogger.warn( `Only one of SEQ_API_KEY (conf.json5) and SEQ_URL (Environment) were set. Not sending logs to Seq, as both must be provided.` ); @@ -224,9 +228,7 @@ function CreateLogCtx(filename: string, lg = rootLogger): KtLogger { context: [replacedFilename], }) as KtLogger; - logger.defaultMeta = Object.assign({}, logger.defaultMeta ?? {}, { - context: [replacedFilename], - }); + logger.defaultMeta = { ...(logger.defaultMeta ?? {}), context: [replacedFilename] }; return logger; } @@ -237,7 +239,7 @@ export function AppendLogCtx(context: string, lg: KtLogger): KtLogger { } export function ChangeRootLogLevel( - level: "crit" | "severe" | "error" | "warn" | "info" | "verbose" | "debug" + level: "crit" | "debug" | "error" | "info" | "severe" | "verbose" | "warn" ) { rootLogger.info(`Changing log level to ${level}.`); diff --git a/server/src/lib/migration/migrations.ts b/server/src/lib/migration/migrations.ts index d85b80a13..3e8f6cc8f 100644 --- a/server/src/lib/migration/migrations.ts +++ b/server/src/lib/migration/migrations.ts @@ -1,8 +1,8 @@ +import UGPTRivalsMigration from "./migrations/add-rivals-to-ugpt"; import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; import { Environment } from "lib/setup/config"; -import { Migration } from "utils/types"; -import UGPTRivalsMigration from "./migrations/add-rivals-to-ugpt"; +import type { Migration } from "utils/types"; const logger = CreateLogCtx(__filename); @@ -18,12 +18,12 @@ 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. -const REGISTERED_MIGRATIONS: Migration[] = +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: Migration[]) { +function CreateMigrationLookupMap(migrations: Array) { const map = new Map(); for (const mig of migrations) { diff --git a/server/src/lib/migration/migrations/add-rivals-to-ugpt.ts b/server/src/lib/migration/migrations/add-rivals-to-ugpt.ts index c7f7ff024..89662bddf 100644 --- a/server/src/lib/migration/migrations/add-rivals-to-ugpt.ts +++ b/server/src/lib/migration/migrations/add-rivals-to-ugpt.ts @@ -1,5 +1,5 @@ import db from "external/mongo/db"; -import { Migration } from "utils/types"; +import type { Migration } from "utils/types"; const migration: Migration = { id: "add-rivals-to-ugpt", diff --git a/server/src/lib/notifications/notification-wrappers.ts b/server/src/lib/notifications/notification-wrappers.ts index eefa62127..584649784 100644 --- a/server/src/lib/notifications/notification-wrappers.ts +++ b/server/src/lib/notifications/notification-wrappers.ts @@ -1,6 +1,7 @@ -import db from "external/mongo/db"; -import { FormatGame, Game, integer, Playtype, PublicUserDocument } from "tachi-common"; import { SendNotification } from "./notifications"; +import db from "external/mongo/db"; +import { FormatGame } from "tachi-common"; +import type { Game, integer, Playtype, PublicUserDocument } from "tachi-common"; /** * A utility wrapper for sending the SET_RIVAL notification. diff --git a/server/src/lib/notifications/notifications.ts b/server/src/lib/notifications/notifications.ts index d655cd6bd..b6f873c6e 100644 --- a/server/src/lib/notifications/notifications.ts +++ b/server/src/lib/notifications/notifications.ts @@ -1,6 +1,6 @@ import db from "external/mongo/db"; -import { integer, NotificationBody, NotificationDocument } from "tachi-common"; import { Random20Hex } from "utils/misc"; +import type { integer, NotificationBody, NotificationDocument } from "tachi-common"; function ConstructNotificationDoc( title: string, @@ -34,7 +34,11 @@ export function SendNotification(title: string, toUserID: integer, body: Notific * Send notifications to multiple users at once. This is more efficient than calling * send notification in parallel. */ -export function BulkSendNotification(title: string, toUserIDs: integer[], body: NotificationBody) { +export function BulkSendNotification( + title: string, + toUserIDs: Array, + body: NotificationBody +) { const notifications = toUserIDs.map((e) => ConstructNotificationDoc(title, e, body)); return db.notifications.insert(notifications); diff --git a/server/src/lib/orphan-queue/orphan-queue.ts b/server/src/lib/orphan-queue/orphan-queue.ts index 38e155927..c75c0abae 100644 --- a/server/src/lib/orphan-queue/orphan-queue.ts +++ b/server/src/lib/orphan-queue/orphan-queue.ts @@ -1,7 +1,9 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; -import { FilterQuery } from "mongodb"; -import { +import { GetNextCounterValue } from "utils/db"; +import { DedupeArr } from "utils/misc"; +import type { FilterQuery } from "mongodb"; +import type { ChartDocument, IDStrings, IDStringToGame, @@ -9,8 +11,6 @@ import { OrphanChart, SongDocument, } from "tachi-common"; -import { GetNextCounterValue } from "utils/db"; -import { DedupeArr } from "utils/misc"; const logger = CreateLogCtx(__filename); @@ -39,7 +39,7 @@ export async function HandleOrphanQueue( logger.debug(`Received orphanqueue request for ${chartName}.`); const orphanChart = await db["orphan-chart-queue"].findOne( - Object.assign({ idString }, orphanMatchCriteria), + { idString, ...orphanMatchCriteria }, { projectID: true, } @@ -63,8 +63,10 @@ export async function HandleOrphanQueue( const uniqueUsersArr = DedupeArr(orphanChart.userIDs); const playcount = uniqueUsersArr.length; + // If N or more people have played this chart while orphaned, unorphan // it. + if (playcount >= queueSize) { logger.info(`Song ${chartName} was unorphaned by userIDs ${uniqueUsersArr.join(", ")}.`); const songID = await GetNextCounterValue(`${game}-song-id`); diff --git a/server/src/lib/rivals/rivals.ts b/server/src/lib/rivals/rivals.ts index 09de27a93..cece54034 100644 --- a/server/src/lib/rivals/rivals.ts +++ b/server/src/lib/rivals/rivals.ts @@ -2,9 +2,10 @@ import db from "external/mongo/db"; import { SetRivalsFailReasons } from "lib/constants/err-codes"; import CreateLogCtx from "lib/logger/logger"; import { SendSetRivalNotification } from "lib/notifications/notification-wrappers"; -import { FormatGame, Game, integer, Playtype } from "tachi-common"; +import { FormatGame } from "tachi-common"; import { ArrayDiff } from "utils/misc"; import { GetUsersWithIDs, GetUserWithIDGuaranteed } from "utils/user"; +import type { Game, integer, Playtype } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -56,7 +57,7 @@ export async function SetRivals( userID: integer, game: Game, playtype: Playtype, - newRivals: integer[] + newRivals: Array ) { if (newRivals.length > 5) { return SetRivalsFailReasons.TOO_MANY; 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 30c72eed1..68d802125 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 @@ -1,4 +1,10 @@ -import { KtLogger } from "lib/logger/logger"; +import { + CalculateKTLampRatingIIDXDP, + CalculateKTLampRatingIIDXSP, + CalculateKTRating, + CalculateMFCP, + CalculateSieglinde, +} from "./stats"; import { CHUNITHMRating, GITADORASkill, @@ -8,23 +14,10 @@ import { Volforce, WACCARate, } from "rg-stats"; -import { - ChartDocument, - Game, - GetGamePTConfig, - IDStrings, - Lamps, - Playtypes, - ScoreDocument, -} from "tachi-common"; -import { DryScore } from "../common/types"; -import { - CalculateKTLampRatingIIDXDP, - CalculateKTLampRatingIIDXSP, - CalculateKTRating, - CalculateMFCP, - CalculateSieglinde, -} from "./stats"; +import { GetGamePTConfig } from "tachi-common"; +import type { DryScore } from "../common/types"; +import type { KtLogger } from "lib/logger/logger"; +import type { ChartDocument, Game, IDStrings, Lamps, Playtypes, ScoreDocument } from "tachi-common"; export async function CreateCalculatedData( dryScore: DryScore, @@ -84,6 +77,7 @@ export async function CalculateDataForGamePT( playtype: Playtypes[G], chart: ChartDocument, dryScore: DryScore, + // ESD gets specially passed through because it's not part of the DryScore, but // can be used for statistics anyway. esd: number | null, @@ -97,7 +91,7 @@ type CalculatedData = Required["calculated function CalculateDataIIDX( dryScore: DryScore, - chart: ChartDocument<"iidx:SP" | "iidx:DP">, + chart: ChartDocument<"iidx:DP" | "iidx:SP">, logger: KtLogger ): CalculatedData<"iidx:SP"> { let bpi; @@ -131,7 +125,7 @@ function CalculateDataSDVXorUSC( dryScore: DryScore, chart: ChartDocument, logger: KtLogger -): CalculatedData<"sdvx:Single" | "usc:Keyboard" | "usc:Controller"> { +): CalculatedData<"sdvx:Single" | "usc:Controller" | "usc:Keyboard"> { // for usc, unofficial charts currently have no VF6 value. if ( dryScore.game === "usc" && @@ -185,7 +179,7 @@ function CalculateDataCHUNITHM( function CalculateDataGitadora( dryScore: DryScore, chart: ChartDocument -): CalculatedData<"gitadora:Gita" | "gitadora:Dora"> { +): CalculatedData<"gitadora:Dora" | "gitadora:Gita"> { return { skill: GITADORASkill.calculate(dryScore.scoreData.percent, chart.levelNum), }; @@ -195,7 +189,7 @@ function CalculateDataDDR( dryScore: DryScore, chart: ChartDocument, logger: KtLogger -): CalculatedData<"ddr:SP" | "ddr:DP"> { +): CalculatedData<"ddr:DP" | "ddr:SP"> { return { MFCP: CalculateMFCP(dryScore, chart, logger), ktRating: CalculateKTRating(dryScore, "ddr", chart.playtype, chart, logger), 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 b17d8bfc3..bf383f4fa 100644 --- a/server/src/lib/score-import/framework/calculated-data/stats.ts +++ b/server/src/lib/score-import/framework/calculated-data/stats.ts @@ -1,6 +1,7 @@ -import { KtLogger } from "lib/logger/logger"; -import { ChartDocument, Game, GetGamePTConfig, IIDX_LAMPS, integer, Playtype } from "tachi-common"; -import { DryScore } from "../common/types"; +import { GetGamePTConfig, IIDX_LAMPS } from "tachi-common"; +import type { DryScore } from "../common/types"; +import type { KtLogger } from "lib/logger/logger"; +import type { ChartDocument, Game, integer, Playtype } from "tachi-common"; /** * Calculate Marvelous Full Combo Points. This algorithm diff --git a/server/src/lib/score-import/framework/common/converter-failures.ts b/server/src/lib/score-import/framework/common/converter-failures.ts index 7130ecb0d..1f9f457f3 100644 --- a/server/src/lib/score-import/framework/common/converter-failures.ts +++ b/server/src/lib/score-import/framework/common/converter-failures.ts @@ -1,7 +1,7 @@ // Various Errors that can occur during processing. -import { ImportTypes } from "tachi-common"; -import { ImportTypeContextMap, ImportTypeDataMap } from "../../import-types/common/types"; +import type { ImportTypeContextMap, ImportTypeDataMap } from "../../import-types/common/types"; +import type { ImportTypes } from "tachi-common"; export class ConverterFailure { message: string | null; 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 8c9af7bc4..a98346d0c 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 @@ -1,12 +1,12 @@ -import { KtLogger } from "lib/logger/logger"; -import { +import { Parsers } from "lib/score-import/import-types/parsers"; +import type { KtLogger } from "lib/logger/logger"; +import type { ImportTypeContextMap, ImportTypeDataMap, ParserFunctionReturns, } from "lib/score-import/import-types/common/types"; -import { Parsers } from "lib/score-import/import-types/parsers"; -import { ScoreImportJobData } from "lib/score-import/worker/types"; -import { ImportTypes } from "tachi-common"; +import type { ScoreImportJobData } from "lib/score-import/worker/types"; +import type { ImportTypes } from "tachi-common"; export function GetInputParser(jobData: ScoreImportJobData) { // Retrieve the set parser function for this import type. diff --git a/server/src/lib/score-import/framework/common/import-logger.ts b/server/src/lib/score-import/framework/common/import-logger.ts index bcda8b602..4f95a38cd 100644 --- a/server/src/lib/score-import/framework/common/import-logger.ts +++ b/server/src/lib/score-import/framework/common/import-logger.ts @@ -1,6 +1,7 @@ -import { KtLogger, rootLogger } from "lib/logger/logger"; -import { ImportTypes, PublicUserDocument } from "tachi-common"; +import { rootLogger } from "lib/logger/logger"; import { FormatUserDoc } from "utils/user"; +import type { KtLogger } from "lib/logger/logger"; +import type { ImportTypes, PublicUserDocument } from "tachi-common"; export function CreateScoreLogger( user: PublicUserDocument, diff --git a/server/src/lib/score-import/framework/common/score-utils.ts b/server/src/lib/score-import/framework/common/score-utils.ts index 70c4c0b90..9238264d9 100644 --- a/server/src/lib/score-import/framework/common/score-utils.ts +++ b/server/src/lib/score-import/framework/common/score-utils.ts @@ -1,15 +1,7 @@ -import CreateLogCtx from "lib/logger/logger"; -import { - ChartDocument, - ESDCore, - Game, - GameToIDStrings, - GetGamePTConfig, - Grades, - IDStrings, - Playtype, -} from "tachi-common"; import { InternalFailure, InvalidScoreFailure } from "./converter-failures"; +import CreateLogCtx from "lib/logger/logger"; +import { ESDCore, GameToIDStrings, GetGamePTConfig } from "tachi-common"; +import type { ChartDocument, Game, Grades, IDStrings, Playtype } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -64,7 +56,7 @@ export function GenericCalculatePercent(game: Game, score: number, chart?: Chart return score; case "bms": case "pms": - case "iidx": + case "iidx": { if (!chart) { logger.severe("No Chart passed to GenericCalcPercent but game was iidx/bms/pms."); throw new InternalFailure( @@ -77,19 +69,22 @@ export function GenericCalculatePercent(game: Game, score: number, chart?: Chart const MAX = ( chart as ChartDocument< - | "iidx:SP" - | "iidx:DP" | "bms:7K" | "bms:14K" + | "iidx:DP" + | "iidx:SP" | "pms:Controller" | "pms:Keyboard" > ).data.notecount * 2; return (100 * score) / MAX; - default: + } + + default: { logger.severe(`Invalid game passed of ${game} to GenericCalcPercent.`); throw new InternalFailure(`Invalid game passed of ${game} to GenericCalcPercent.`); + } } } @@ -108,6 +103,7 @@ export function ValidatePercent( // i love needing a helper function for *ONE* game. if (game === "maimai") { const mmChart = chart as ChartDocument<"maimai:Single">; + if (percent > mmChart.data.maxPercent) { throw new InvalidScoreFailure( `Invalid percent - expected a number less than ${mmChart.data.maxPercent}.` @@ -144,7 +140,7 @@ export function GenericGetGradeAndPercent( ValidatePercent(game, chart.playtype, percent, chart); - const grade = GetGradeFromPercent(game, chart.playtype, percent) as Grades[GameToIDStrings[G]]; + const grade = GetGradeFromPercent(game, chart.playtype, percent); return { percent, grade }; } @@ -193,7 +189,7 @@ export function CalculateESDForGame( * Parses and validates a date from a string. * @returns Milliseconds from the unix epoch, or null if the initial argument was null or undefined. */ -export function ParseDateFromString(str: string | undefined | null): number | null { +export function ParseDateFromString(str: string | null | undefined): number | null { if (!str) { return null; } diff --git a/server/src/lib/score-import/framework/common/string-asserts.ts b/server/src/lib/score-import/framework/common/string-asserts.ts index 4b0b77978..e75bd7a11 100644 --- a/server/src/lib/score-import/framework/common/string-asserts.ts +++ b/server/src/lib/score-import/framework/common/string-asserts.ts @@ -1,5 +1,7 @@ -import { Difficulties, Game, GetGamePTConfig, IDStrings, Playtype } from "tachi-common"; import { InvalidScoreFailure } from "./converter-failures"; +import { GetGamePTConfig } from "tachi-common"; +import type { Difficulties, Game, IDStrings, Playtype } from "tachi-common"; + export function AssertStrAsDifficulty( strVal: string, game: Game, diff --git a/server/src/lib/score-import/framework/common/types.ts b/server/src/lib/score-import/framework/common/types.ts index e2abb7d63..746400ae4 100644 --- a/server/src/lib/score-import/framework/common/types.ts +++ b/server/src/lib/score-import/framework/common/types.ts @@ -1,4 +1,5 @@ -import { IDStrings, ScoreDocument, Playtype, Game } from "tachi-common"; +import { Game } from "tachi-common"; +import type { IDStrings, ScoreDocument, Playtype } from "tachi-common"; /** * An intermediate score format that will be filled out by @@ -6,9 +7,9 @@ import { IDStrings, ScoreDocument, Playtype, Game } from "tachi-common"; */ export type DryScore = Pick< ScoreDocument, - "service" | "game" | "scoreMeta" | "timeAchieved" | "comment" | "importType" + "comment" | "game" | "importType" | "scoreMeta" | "service" | "timeAchieved" > & { - scoreData: Omit["scoreData"], "gradeIndex" | "lampIndex" | "esd">; + scoreData: Omit["scoreData"], "esd" | "gradeIndex" | "lampIndex">; }; -export type ScorePlaytypeMap = Partial>; +export type ScorePlaytypeMap = Partial>>; diff --git a/server/src/lib/score-import/framework/express-wrapper.ts b/server/src/lib/score-import/framework/express-wrapper.ts index a7de7c989..680644557 100644 --- a/server/src/lib/score-import/framework/express-wrapper.ts +++ b/server/src/lib/score-import/framework/express-wrapper.ts @@ -1,15 +1,15 @@ +import { MakeScoreImport } from "./score-import"; +import ScoreImportFatalError from "./score-importing/score-import-error"; import CreateLogCtx from "lib/logger/logger"; -import { +import { Random20Hex } from "utils/misc"; +import type { ParserArguments } from "../worker/types"; +import type { ImportDocument, ImportTypes, integer, SuccessfulAPIResponse, UnsuccessfulAPIResponse, } from "tachi-common"; -import { Random20Hex } from "utils/misc"; -import { ParserArguments } from "../worker/types"; -import { MakeScoreImport } from "./score-import"; -import ScoreImportFatalError from "./score-importing/score-import-error"; export interface WrappedAPIResponse { statusCode: number; diff --git a/server/src/lib/score-import/framework/goals/goals.ts b/server/src/lib/score-import/framework/goals/goals.ts index 234721477..e8b9b6310 100644 --- a/server/src/lib/score-import/framework/goals/goals.ts +++ b/server/src/lib/score-import/framework/goals/goals.ts @@ -1,8 +1,8 @@ -import { integer, Game, GoalDocument, GoalSubscriptionDocument } from "tachi-common"; -import { EvaluateGoalForUser } from "lib/targets/goals"; import db from "external/mongo/db"; -import { KtLogger } from "lib/logger/logger"; +import { EvaluateGoalForUser } from "lib/targets/goals"; import { EmitWebhookEvent } from "lib/webhooks/webhooks"; +import type { KtLogger } from "lib/logger/logger"; +import type { integer, Game, GoalDocument, GoalSubscriptionDocument } from "tachi-common"; /** * Update a user's progress on all of their set goals. @@ -26,7 +26,7 @@ export async function GetAndUpdateUsersGoals( } export async function UpdateGoalsForUser( - goals: GoalDocument[], + goals: Array, goalSubsMap: Map, userID: integer, logger: KtLogger @@ -122,7 +122,9 @@ export async function ProcessGoal( }; let webhookEvent = null; + // if this is a newly-achieved goal + if (res.achieved && !goalSub.achieved) { webhookEvent = { goalID: goal.goalID, @@ -135,6 +137,7 @@ export async function ProcessGoal( const setData = { ...newData, timeAchieved: newData.achieved ? Date.now() : null, + // we're guaranteed that this works, because things // that haven't changed return nothing instead of // getting to this point. @@ -187,7 +190,7 @@ export async function GetRelevantGoals( userID: integer, chartIDs: Set, logger: KtLogger -): Promise<{ goals: GoalDocument[]; goalSubsMap: Map }> { +): Promise<{ goals: Array; goalSubsMap: Map }> { const goalSubs = await db["goal-subs"].find({ game, userID }, { projectID: true }); logger.verbose(`Found user has ${goalSubs.length} goals.`); @@ -198,7 +201,8 @@ export async function GetRelevantGoals( const goalIDs = goalSubs.map((e) => e.goalID); - const chartIDsArr: string[] = []; + const chartIDsArr: Array = []; + for (const c of chartIDs) { chartIDsArr.push(c); } @@ -225,6 +229,7 @@ export async function GetRelevantGoals( if (!goalSet.has(goalSub.goalID)) { continue; } + // since these are guaranteed to be unique, lets make a hot map of goalID -> goalSubDocument, so we can // pull them in for post-processing and filter out the goalSubDocuments that aren't relevant. goalSubsMap.set(goalSub.goalID, goalSub); @@ -240,7 +245,7 @@ export async function GetRelevantGoals( * Returns the set of goals where its folder contains any member * of chartIDsArr. */ -export function GetRelevantFolderGoals(goalIDs: string[], chartIDsArr: string[]) { +export function GetRelevantFolderGoals(goalIDs: Array, chartIDsArr: Array) { // Slightly black magic - this is kind of like doing an SQL join. // it's weird to do this in mongodb, but this seems like the right // way to actually handle this. diff --git a/server/src/lib/score-import/framework/import-locks/lock.ts b/server/src/lib/score-import/framework/import-locks/lock.ts index be82069a5..419f818a1 100644 --- a/server/src/lib/score-import/framework/import-locks/lock.ts +++ b/server/src/lib/score-import/framework/import-locks/lock.ts @@ -1,5 +1,5 @@ import db from "external/mongo/db"; -import { integer } from "tachi-common"; +import type { integer } from "tachi-common"; /** * If a user has no ongoing import, enable the import lock and return true. diff --git a/server/src/lib/score-import/framework/milestones/milestones.ts b/server/src/lib/score-import/framework/milestones/milestones.ts index 2fde5e0f4..5b63c464e 100644 --- a/server/src/lib/score-import/framework/milestones/milestones.ts +++ b/server/src/lib/score-import/framework/milestones/milestones.ts @@ -1,8 +1,9 @@ import db from "external/mongo/db"; -import { KtLogger } from "lib/logger/logger"; +import { CalculateMilestoneOutOf, GetGoalIDsFromMilestone } from "lib/targets/milestones"; import { EmitWebhookEvent } from "lib/webhooks/webhooks"; -import { BulkWriteUpdateOneOperation } from "mongodb"; -import { +import type { KtLogger } from "lib/logger/logger"; +import type { BulkWriteUpdateOneOperation } from "mongodb"; +import type { Game, GoalImportInfo, integer, @@ -11,7 +12,6 @@ import { Playtype, MilestoneSubscriptionDocument, } from "tachi-common"; -import { CalculateMilestoneOutOf, GetGoalIDsFromMilestone } from "lib/targets/milestones"; /** * Processes and updates a user's milestones from their Goal Import Info (i.e. what is returned @@ -50,15 +50,16 @@ export function ProcessMilestoneFromGII( } export async function UpdateUsersMilestones( - importGoalInfo: GoalImportInfo[], + importGoalInfo: Array, game: Game, - playtypes: Playtype[], + playtypes: Array, userID: integer, logger: KtLogger ) { const goalSubInfoMap: Map = new Map(); const goalIDs = []; + for (const e of importGoalInfo) { goalSubInfoMap.set(e.goalID, e.new); goalIDs.push(e.goalID); @@ -75,6 +76,7 @@ export async function UpdateUsersMilestones( // create a map here to avoid linear searching when // co-iterating const milestoneSubMap = new Map(); + for (const um of milestoneSubs) { milestoneSubMap.set(um.milestoneID, um); } @@ -85,9 +87,9 @@ export async function UpdateUsersMilestones( importGoalMap.set(ig.goalID, ig.new); } - const bwrite: BulkWriteUpdateOneOperation[] = []; + const bwrite: Array> = []; - const importMilestoneInfo: MilestoneImportInfo[] = []; + const importMilestoneInfo: Array = []; for (const milestone of milestones) { const { achieved, progress } = ProcessMilestoneFromGII(milestone, importGoalMap); @@ -128,6 +130,7 @@ export async function UpdateUsersMilestones( if (progress !== milestoneSub.progress) { importMilestoneInfo.push(milestoneInfo); + // @ts-expect-error This property isn't read only, because I said so. bwriteOp.updateOne.update.$set!.lastInteraction = Date.now(); } @@ -155,9 +158,9 @@ export async function UpdateUsersMilestones( } async function GetRelevantMilestones( - goalIDs: string[], + goalIDs: Array, game: Game, - playtypes: Playtype[], + playtypes: Array, userID: integer, logger: KtLogger ) { diff --git a/server/src/lib/score-import/framework/orphans/orphans.ts b/server/src/lib/score-import/framework/orphans/orphans.ts index ddc0e7063..2b3c04731 100644 --- a/server/src/lib/score-import/framework/orphans/orphans.ts +++ b/server/src/lib/score-import/framework/orphans/orphans.ts @@ -1,23 +1,23 @@ -import db from "external/mongo/db"; -import { - ConverterFunction, - ConverterFnReturnOrFailure, - ImportTypeContextMap, - ImportTypeDataMap, - OrphanScoreDocument, -} from "../../import-types/common/types"; -import { Game, ImportTypes, integer } from "tachi-common"; -import fjsh from "fast-json-stable-hash"; -import { KtLogger } from "lib/logger/logger"; import { Converters } from "../../import-types/converters"; import { ConverterFailure, InternalFailure, KTDataNotFoundFailure, } from "../common/converter-failures"; -import { ProcessSuccessfulConverterReturn } from "../score-importing/score-importing"; import { HandlePostImportSteps } from "../score-importing/score-import-main"; +import { ProcessSuccessfulConverterReturn } from "../score-importing/score-importing"; +import db from "external/mongo/db"; +import fjsh from "fast-json-stable-hash"; import { GetUserWithID } from "utils/user"; +import type { + ConverterFunction, + ConverterFnReturnOrFailure, + ImportTypeContextMap, + ImportTypeDataMap, + OrphanScoreDocument, +} from "../../import-types/common/types"; +import type { KtLogger } from "lib/logger/logger"; +import type { Game, ImportTypes, integer } from "tachi-common"; /** * Creates an OrphanedScore document from the data and context, @@ -34,7 +34,7 @@ export async function OrphanScore( game: Game, logger: KtLogger ) { - const orphan: Pick = { + const orphan: Pick = { importType, data, context, @@ -44,6 +44,7 @@ export async function OrphanScore( logger.debug("Orphaning document", orphan); let orphanID; + try { orphanID = `O${fjsh.hash(orphan, "sha256")}`; } catch (err) { @@ -83,7 +84,7 @@ export async function OrphanScore( */ export async function ReprocessOrphan( orphan: OrphanScoreDocument, - blacklist: string[], + blacklist: Array, logger: KtLogger ) { const ConverterFunction = Converters[orphan.importType] as ConverterFunction< 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 46c323e6c..6058a3bf5 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 @@ -1,7 +1,3 @@ -import db from "external/mongo/db"; -import { KtLogger } from "lib/logger/logger"; -import { BulkWriteUpdateOneOperation } from "mongodb"; -import { integer, PBScoreDocument, ScoreDocument } from "tachi-common"; import { BMSMergeFn, IIDXMergeFn, @@ -9,6 +5,10 @@ import { SDVXMergeFn, USCMergeFn, } from "./game-specific-merge"; +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"; export type PBScoreDocumentNoRank = Omit; @@ -75,12 +75,13 @@ export async function UpdateChartRanking(chartID: string) { } ); - const bwrite: BulkWriteUpdateOneOperation[] = []; + const bwrite: Array> = []; let rank = 0; for (let i = 0; i < scores.length; i++) { const score = scores[i]; + rank++; bwrite.push({ @@ -165,6 +166,7 @@ async function MergeScoreLampIntoPB( }; const GameSpecificMergeFn = GAME_SPECIFIC_MERGE_FNS[scorePB.game]; + if (GameSpecificMergeFn) { const success = await GameSpecificMergeFn(pbDoc, scorePB, lampPB, logger); 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 1902c6af8..b657a3f7a 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 @@ -1,14 +1,14 @@ -import db from "external/mongo/db"; -import { KtLogger } from "lib/logger/logger"; -import { Volforce } from "rg-stats"; -import { PBScoreDocument, ScoreDocument } from "tachi-common"; -import { FindChartWithChartID } from "utils/queries/charts"; import { InternalFailure } from "../common/converter-failures"; +import db from "external/mongo/db"; +import { Volforce } from "rg-stats"; +import { FindChartWithChartID } from "utils/queries/charts"; +import type { KtLogger } from "lib/logger/logger"; +import type { PBScoreDocument, ScoreDocument } from "tachi-common"; export async function IIDXMergeFn( - pbDoc: PBScoreDocument<"iidx:SP" | "iidx:DP">, - scorePB: ScoreDocument<"iidx:SP" | "iidx:DP">, - lampPB: ScoreDocument<"iidx:SP" | "iidx:DP">, + pbDoc: PBScoreDocument<"iidx:DP" | "iidx:SP">, + scorePB: ScoreDocument<"iidx:DP" | "iidx:SP">, + lampPB: ScoreDocument<"iidx:DP" | "iidx:SP">, logger: KtLogger ): Promise { // lampRating needs to be updated. @@ -26,13 +26,14 @@ export async function IIDXMergeFn( "scoreData.hitMeta.bp": 1, // bp 0 is the best BP, bp 1 is worse, so on }, } - )) as ScoreDocument<"iidx:SP" | "iidx:DP">; + )) as ScoreDocument<"iidx:DP" | "iidx:SP">; if (!bpPB) { logger.verbose( `Could not find BP PB for ${scorePB.userID} ${scorePB.chartID} in PB joining. User likely has no scores with BP defined.`, { pbDoc } ); + // this isn't actually an error! we just don't have to do anything. return true; } @@ -87,9 +88,9 @@ export function BMSMergeFn( * This is near-identical to the SDVXMergeFn. See that. */ export async function USCMergeFn( - pbDoc: PBScoreDocument<"usc:Keyboard" | "usc:Controller">, - scorePB: ScoreDocument<"usc:Keyboard" | "usc:Controller">, - lampPB: ScoreDocument<"usc:Keyboard" | "usc:Controller">, + pbDoc: PBScoreDocument<"usc:Controller" | "usc:Keyboard">, + scorePB: ScoreDocument<"usc:Controller" | "usc:Keyboard">, + lampPB: ScoreDocument<"usc:Controller" | "usc:Keyboard">, logger: KtLogger ) { // @optimisable - see SDVXMergeFn diff --git a/server/src/lib/score-import/framework/pb/process-pbs.ts b/server/src/lib/score-import/framework/pb/process-pbs.ts index d6a9af03d..2a51addb7 100644 --- a/server/src/lib/score-import/framework/pb/process-pbs.ts +++ b/server/src/lib/score-import/framework/pb/process-pbs.ts @@ -1,7 +1,8 @@ +import { CreatePBDoc, UpdateChartRanking } from "./create-pb-doc"; import db from "external/mongo/db"; -import { KtLogger } from "lib/logger/logger"; -import { integer } from "tachi-common"; -import { CreatePBDoc, PBScoreDocumentNoRank, UpdateChartRanking } from "./create-pb-doc"; +import type { PBScoreDocumentNoRank } from "./create-pb-doc"; +import type { KtLogger } from "lib/logger/logger"; +import type { integer } from "tachi-common"; /** * Process, recalculate and update a users PBs for this set of chartIDs. @@ -23,7 +24,7 @@ export async function ProcessPBs( const pbDocsReturn = await Promise.all(promises); - const pbDocs: PBScoreDocumentNoRank[] = []; + const pbDocs: Array = []; for (const doc of pbDocsReturn) { if (!doc) { diff --git a/server/src/lib/score-import/framework/score-import.ts b/server/src/lib/score-import/framework/score-import.ts index f72b0a25c..9e5c3f9fd 100644 --- a/server/src/lib/score-import/framework/score-import.ts +++ b/server/src/lib/score-import/framework/score-import.ts @@ -1,14 +1,14 @@ /* eslint-disable no-await-in-loop */ -import { JOB_RETRY_COUNT } from "lib/constants/tachi"; -import CreateLogCtx from "lib/logger/logger"; -import { ServerConfig } from "lib/setup/config"; -import { ImportDocument, ImportTypes, integer } from "tachi-common"; -import { Sleep } from "utils/misc"; -import ScoreImportQueue, { ScoreImportQueueEvents } from "../worker/queue"; -import { ScoreImportJobData } from "../worker/types"; import { GetInputParser } from "./common/get-input-parser"; import ScoreImportFatalError from "./score-importing/score-import-error"; import ScoreImportMain from "./score-importing/score-import-main"; +import ScoreImportQueue, { ScoreImportQueueEvents } from "../worker/queue"; +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 { ImportDocument, ImportTypes, integer } from "tachi-common"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/score-import/framework/score-importing/hydrate-score.ts b/server/src/lib/score-import/framework/score-importing/hydrate-score.ts index 6f8aad22c..8238a0b90 100644 --- a/server/src/lib/score-import/framework/score-importing/hydrate-score.ts +++ b/server/src/lib/score-import/framework/score-importing/hydrate-score.ts @@ -1,8 +1,9 @@ -import { ChartDocument, integer, ScoreDocument, SongDocument, GetGamePTConfig } from "tachi-common"; -import { KtLogger } from "lib/logger/logger"; import { CreateCalculatedData } from "../calculated-data/calculated-data"; import { CalculateESDForGame } from "../common/score-utils"; -import { DryScore } from "../common/types"; +import { GetGamePTConfig } from "tachi-common"; +import type { DryScore } from "../common/types"; +import type { KtLogger } from "lib/logger/logger"; +import type { ChartDocument, integer, ScoreDocument, SongDocument } from "tachi-common"; /** * Takes an "intermediate" score and appends the rest of the data it needs. @@ -30,20 +31,20 @@ export async function HydrateScore( const gptConfig = GetGamePTConfig(dryScore.game, chart.playtype); // Fill out the rest of the fields we want for scoreData - const scoreData = Object.assign( - { - lampIndex: gptConfig.lamps.indexOf(dryScore.scoreData.lamp), - gradeIndex: gptConfig.grades.indexOf(dryScore.scoreData.grade), - esd, - }, - dryScoreData - ); + const scoreData = { + lampIndex: gptConfig.lamps.indexOf(dryScore.scoreData.lamp), + gradeIndex: gptConfig.grades.indexOf(dryScore.scoreData.grade), + esd, + ...dryScoreData, + }; const score: ScoreDocument = { // extract all of the non-scoreData props from a dry score and push them here ...rest, + // then push our score data. scoreData, + // everything below this point is sane highlight: false, timeAdded: Date.now(), 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 5e83d341e..10c653d23 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 @@ -1,12 +1,12 @@ -import { ScoreDocument, integer } from "tachi-common"; import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; +import type { ScoreDocument, integer } from "tachi-common"; const logger = CreateLogCtx(__filename); const MAX_PIPELINE_LENGTH = 500; interface ScoreQueue { - queue: ScoreDocument[]; + queue: Array; scoreIDSet: Set; } diff --git a/server/src/lib/score-import/framework/score-importing/score-id.ts b/server/src/lib/score-import/framework/score-importing/score-id.ts index 67c780d67..43bb41642 100644 --- a/server/src/lib/score-import/framework/score-importing/score-id.ts +++ b/server/src/lib/score-import/framework/score-importing/score-id.ts @@ -1,7 +1,7 @@ -import crypto from "crypto"; import db from "external/mongo/db"; -import { integer } from "tachi-common"; -import { DryScore } from "../common/types"; +import crypto from "crypto"; +import type { DryScore } from "../common/types"; +import type { integer } from "tachi-common"; /** * Performs sha256 hashing on the input data. 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 8395f613f..f027d3403 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 @@ -1,9 +1,25 @@ +import ScoreImportFatalError from "./score-import-error"; +import { ImportAllIterableData } from "./score-importing"; +import { Converters } from "../../import-types/converters"; +import { InternalFailure } from "../common/converter-failures"; +import { CreateScoreLogger } from "../common/import-logger"; +import { GetAndUpdateUsersGoals } from "../goals/goals"; +import { CheckAndSetOngoingImportLock, UnsetOngoingImportLock } from "../import-locks/lock"; +import { UpdateUsersMilestones } from "../milestones/milestones"; +import { ProcessPBs } from "../pb/process-pbs"; +import { CreateSessions } from "../sessions/sessions"; +import { UpdateUsersGamePlaytypeStats } from "../user-game-stats/update-ugs"; import db from "external/mongo/db"; -import { KtLogger } from "lib/logger/logger"; -import { ScoreImportJob } from "lib/score-import/worker/types"; -import { +import { GetGameConfig } from "tachi-common"; +import { GetMillisecondsSince } from "utils/misc"; +import { GetUserWithID } from "utils/user"; +import type { ConverterFunction, ImportInputParser } from "../../import-types/common/types"; +import type { ScorePlaytypeMap } from "../common/types"; +import type { ClassHandler } from "../user-game-stats/types"; +import type { KtLogger } from "lib/logger/logger"; +import type { ScoreImportJob } from "lib/score-import/worker/types"; +import type { Game, - GetGameConfig, IDStrings, ImportDocument, ImportProcessingInfo, @@ -12,22 +28,6 @@ import { Playtype, PublicUserDocument, } from "tachi-common"; -import { GetMillisecondsSince } from "utils/misc"; -import { GetUserWithID } from "utils/user"; -import { ConverterFunction, ImportInputParser } from "../../import-types/common/types"; -import { Converters } from "../../import-types/converters"; -import { InternalFailure } from "../common/converter-failures"; -import { CreateScoreLogger } from "../common/import-logger"; -import { ScorePlaytypeMap } from "../common/types"; -import { GetAndUpdateUsersGoals } from "../goals/goals"; -import { CheckAndSetOngoingImportLock, UnsetOngoingImportLock } from "../import-locks/lock"; -import { UpdateUsersMilestones } from "../milestones/milestones"; -import { ProcessPBs } from "../pb/process-pbs"; -import { CreateSessions } from "../sessions/sessions"; -import { ClassHandler } from "../user-game-stats/types"; -import { UpdateUsersGamePlaytypeStats } from "../user-game-stats/update-ugs"; -import ScoreImportFatalError from "./score-import-error"; -import { ImportAllIterableData } from "./score-importing"; /** * Performs a Score Import. @@ -68,6 +68,7 @@ export default async function ScoreImportMain( if (hasNoOngoingImport) { logger.info(`User ${userID} made an import while they had one ongoing.`); + // @danger // Throwing away an import if the user already has one outgoing is *bad*, as in the case // of degraded performance we might just start throwing scores away. @@ -186,7 +187,7 @@ export default async function ScoreImportMain( // Create and Save an import document to the database, and finish everything up! const ImportDocument: ImportDocument = { importType, - idStrings: playtypes.map((e) => `${game}:${e}`) as IDStrings[], + idStrings: playtypes.map((e) => `${game}:${e}`) as Array, scoreIDs, playtypes, game, @@ -255,7 +256,7 @@ export default async function ScoreImportMain( * and updating a users game stats. */ export async function HandlePostImportSteps( - importInfo: ImportProcessingInfo[], + importInfo: Array, user: PublicUserDocument, importType: ImportTypes, game: Game, @@ -296,6 +297,7 @@ export async function HandlePostImportSteps( // This function also handles conjoining different scores together (such as unioning best lamp and // best score). const pbTimeStart = process.hrtime.bigint(); + await ProcessPBs(user.id, chartIDs, logger); const pbTime = GetMillisecondsSince(pbTimeStart); @@ -303,7 +305,7 @@ export async function HandlePostImportSteps( logger.debug(`PB Processing took ${pbTime} milliseconds (${pbTimeRel}ms/doc)`); - const playtypes = Object.keys(scorePlaytypeMap) as Playtype[]; + const playtypes = Object.keys(scorePlaytypeMap) as Array; SetJobProgress(job, "Updating profile statistics."); @@ -368,7 +370,7 @@ export async function HandlePostImportSteps( */ async function UpdateUsersGameStats( game: Game, - modifiedPlaytypes: Playtype[], + modifiedPlaytypes: Array, userID: integer, classHandler: ClassHandler | null, logger: KtLogger @@ -387,6 +389,7 @@ async function UpdateUsersGameStats( } const r = await Promise.all(promises); + return r.flat(1); } @@ -397,7 +400,7 @@ async function UpdateUsersGameStats( * A set of unique chartIDs involved in the import and the scores mapped * on their playtype. */ -function ParseImportInfo(importInfo: ImportProcessingInfo[]) { +function ParseImportInfo(importInfo: Array) { const scorePlaytypeMap: ScorePlaytypeMap = Object.create(null); const scoreIDs = []; 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 372d3b2cd..873dbb391 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 @@ -1,21 +1,6 @@ -import db from "external/mongo/db"; -import { AppendLogCtx, KtLogger } from "lib/logger/logger"; -import { ScoreImportJob } from "lib/score-import/worker/types"; -import { - ChartDocument, - Game, - IDStrings, - ImportProcessingInfo, - ImportTypes, - integer, - ScoreDocument, - SongDocument, -} from "tachi-common"; -import { - ConverterFnReturnOrFailure, - ConverterFnSuccessReturn, - ConverterFunction, -} from "../../import-types/common/types"; +import { HydrateScore } from "./hydrate-score"; +import { GetScoreQueueMaybe, InsertQueue, QueueScoreInsert } from "./insert-score"; +import { CreateScoreID } from "./score-id"; import { ConverterFailure, InternalFailure, @@ -23,11 +8,27 @@ import { KTDataNotFoundFailure, SkipScoreFailure, } from "../common/converter-failures"; -import { DryScore } from "../common/types"; import { OrphanScore } from "../orphans/orphans"; -import { HydrateScore } from "./hydrate-score"; -import { GetScoreQueueMaybe, InsertQueue, QueueScoreInsert } from "./insert-score"; -import { CreateScoreID } from "./score-id"; +import db from "external/mongo/db"; +import { AppendLogCtx } from "lib/logger/logger"; +import { IDStrings } from "tachi-common"; +import type { + ConverterFnReturnOrFailure, + ConverterFnSuccessReturn, + ConverterFunction, +} from "../../import-types/common/types"; +import type { DryScore } from "../common/types"; +import type { KtLogger } from "lib/logger/logger"; +import type { ScoreImportJob } from "lib/score-import/worker/types"; +import type { + ChartDocument, + Game, + ImportProcessingInfo, + ImportTypes, + integer, + ScoreDocument, + SongDocument, +} from "tachi-common"; /** * Processes the iterable data into the Tachi database. @@ -40,13 +41,13 @@ import { CreateScoreID } from "./score-id"; export async function ImportAllIterableData( userID: integer, importType: ImportTypes, - iterableData: Iterable | AsyncIterable, + iterableData: AsyncIterable | Iterable, ConverterFunction: ConverterFunction, context: C, game: Game, logger: KtLogger, job: ScoreImportJob | undefined -): Promise { +): Promise> { logger.verbose("Getting Blacklist..."); // @optimisable: could filter harder with score.game and score.playtype @@ -92,9 +93,7 @@ export async function ImportAllIterableData( logger.verbose(`Finished Importing Data (${processedResults.length} datapoints).`); logger.debug(`Removing null returns...`); - const datapoints = processedResults.filter( - (e) => e !== null - ) as ImportProcessingInfo[]; + const datapoints = processedResults.filter((e) => e !== null) as Array; logger.debug(`Removed null from results.`); @@ -126,7 +125,7 @@ export async function ImportIterableDatapoint( ConverterFunction: ConverterFunction, context: C, game: Game, - blacklist: string[], + blacklist: Array, logger: KtLogger ): Promise { // Converter Function Return @@ -198,23 +197,24 @@ export async function ImportIterableDatapoint( return { success: false, type: "InternalError", + // could return cfnReturn.message here, but we might want to hide the details of the crash. message: "An internal error has occured.", content: {}, }; } else if (cfnReturn instanceof SkipScoreFailure) { return null; - } else { - logger.warn(`Unknown error returned as ConverterFailure, Ignoring.`, { - err: cfnReturn, - }); - return { - success: false, - type: "InternalError", - message: "An internal service error has occured.", - content: {}, - }; } + + logger.warn(`Unknown error returned as ConverterFailure, Ignoring.`, { + err: cfnReturn, + }); + return { + success: false, + type: "InternalError", + message: "An internal service error has occured.", + content: {}, + }; } if (cfnReturn instanceof Error) { @@ -229,18 +229,13 @@ export async function ImportIterableDatapoint( }; } - return ProcessSuccessfulConverterReturn( - userID, - cfnReturn as ConverterFnSuccessReturn, - blacklist, - logger - ); + return ProcessSuccessfulConverterReturn(userID, cfnReturn, blacklist, logger); } export async function ProcessSuccessfulConverterReturn( userID: integer, cfnReturn: ConverterFnSuccessReturn, - blacklist: string[], + blacklist: Array, logger: KtLogger, forceImmediateImport = false ): Promise { @@ -289,7 +284,7 @@ async function HydrateAndInsertScore( dryScore: DryScore, chart: ChartDocument, song: SongDocument, - blacklist: string[], + blacklist: Array, importLogger: KtLogger, force = false ): Promise { @@ -333,6 +328,7 @@ async function HydrateAndInsertScore( const score = await HydrateScore(userID, dryScore, chart, song, scoreID, logger); let res; + if (force) { res = await db.scores.insert(score); } else { 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 92c637166..5dc4ee5aa 100644 --- a/server/src/lib/score-import/framework/sessions/calculated-data.ts +++ b/server/src/lib/score-import/framework/sessions/calculated-data.ts @@ -1,11 +1,11 @@ -import { Game, Playtype, Playtypes, ScoreDocument, SessionDocument } from "tachi-common"; +import type { Game, Playtype, Playtypes, ScoreDocument, SessionDocument } from "tachi-common"; type ScoreCalculatedDataOnly = Pick; const RELEVANT_SCORES = 10; -function AverageBest10(vals: (number | null | undefined)[]) { - const numbers = vals.filter((e) => typeof e === "number") as number[]; +function AverageBest10(vals: Array) { + const numbers = vals.filter((e) => typeof e === "number") as Array; if (numbers.length < RELEVANT_SCORES) { return null; @@ -19,24 +19,27 @@ function AverageBest10(vals: (number | null | undefined)[]) { ); } -function SumAll(arr: ScoreCalculatedDataOnly[], prop: keyof ScoreDocument["calculatedData"]) { +function SumAll(arr: Array, prop: keyof ScoreDocument["calculatedData"]) { return arr.reduce((a, e) => { if (typeof e.calculatedData[prop] === "number") { - return a + (e.calculatedData[prop] as number); + return a + e.calculatedData[prop]!; } return a; }, 0); } -function AvgBest10Map(arr: ScoreCalculatedDataOnly[], prop: keyof ScoreDocument["calculatedData"]) { +function AvgBest10Map( + arr: Array, + prop: keyof ScoreDocument["calculatedData"] +) { return AverageBest10(arr.map((e) => e.calculatedData[prop])); } type CalculatedDataFunctions = { [G in Game]: { [P in Playtypes[G]]: ( - scoreCalcData: ScoreCalculatedDataOnly[] + scoreCalcData: Array ) => SessionDocument["calculatedData"]; }; }; @@ -147,7 +150,7 @@ const CalculatedDataFunctions: CalculatedDataFunctions = { export function CreateSessionCalcData( game: Game, playtype: Playtype, - scoreCalcData: ScoreCalculatedDataOnly[] + scoreCalcData: Array ): SessionDocument["calculatedData"] { // @ts-expect-error standard game->pt stuff. return CalculatedDataFunctions[game][playtype](scoreCalcData); diff --git a/server/src/lib/score-import/framework/sessions/sessions.ts b/server/src/lib/score-import/framework/sessions/sessions.ts index 619cb550d..68d662780 100644 --- a/server/src/lib/score-import/framework/sessions/sessions.ts +++ b/server/src/lib/score-import/framework/sessions/sessions.ts @@ -1,7 +1,12 @@ -import crypto from "crypto"; +import { CreateSessionCalcData } from "./calculated-data"; +import { GenerateRandomSessionName } from "./name-generation"; import db from "external/mongo/db"; -import { AppendLogCtx, KtLogger } from "lib/logger/logger"; -import { +import { AppendLogCtx } from "lib/logger/logger"; +import { GetScoresFromSession } from "utils/session"; +import crypto from "crypto"; +import type { ScorePlaytypeMap } from "../common/types"; +import type { KtLogger } from "lib/logger/logger"; +import type { Game, ImportTypes, integer, @@ -12,10 +17,6 @@ import { SessionInfoReturn, SessionScoreInfo, } from "tachi-common"; -import { GetScoresFromSession } from "utils/session"; -import { ScorePlaytypeMap } from "../common/types"; -import { CreateSessionCalcData } from "./calculated-data"; -import { GenerateRandomSessionName } from "./name-generation"; const TWO_HOURS = 1000 * 60 * 60 * 2; @@ -31,7 +32,7 @@ export async function CreateSessions( /* eslint-disable no-await-in-loop */ for (const playtype in scorePtMap) { // @ts-expect-error This is my least favourite thing about ts. - const scores = scorePtMap[playtype] as ScoreDocument[]; + const scores = scorePtMap[playtype] as Array; const sessionInfo = await LoadScoresIntoSessions( userID, @@ -80,9 +81,9 @@ export function CreateSessionID() { function UpdateExistingSession( existingSession: SessionDocument, - newInfo: SessionScoreInfo[], - oldScores: ScoreDocument[], - newScores: ScoreDocument[] + newInfo: Array, + oldScores: Array, + newScores: Array ) { const allScores = [...oldScores, ...newScores]; @@ -109,8 +110,8 @@ function UpdateExistingSession( function CreateSession( userID: integer, importType: ImportTypes, - groupInfo: SessionScoreInfo[], - groupScores: ScoreDocument[], + groupInfo: Array, + groupScores: Array, game: Game, playtype: Playtype ): SessionDocument { @@ -139,11 +140,11 @@ function CreateSession( export async function LoadScoresIntoSessions( userID: integer, importType: ImportTypes, - importScores: ScoreDocument[], + importScores: Array, game: Game, playtype: Playtype, baseLogger: KtLogger -): Promise { +): Promise> { const logger = AppendLogCtx("Session Generation", baseLogger); const timestampedScores = []; @@ -151,6 +152,7 @@ export async function LoadScoresIntoSessions( for (const score of importScores) { if (!score.timeAchieved) { logger.verbose(`Ignored score ${score.scoreID}, as it had no timeAchieved.`); + // ignore scores without timestamps. We can't use these for sessions. continue; } @@ -170,8 +172,8 @@ export async function LoadScoresIntoSessions( // The "Score Groups" for the array of scores provided. // This contains scores split on 2hr margins, which allows for more optimised // session db requests. - const sessionScoreGroups: ScoreDocument[][] = []; - let curGroup: ScoreDocument[] = []; + const sessionScoreGroups: Array> = []; + let curGroup: Array = []; let lastTimestamp = 0; for (const score of timestampedScores) { @@ -181,6 +183,7 @@ export async function LoadScoresIntoSessions( sessionScoreGroups.push(curGroup); curGroup = [score]; } + lastTimestamp = score.timeAchieved!; } @@ -190,7 +193,7 @@ export async function LoadScoresIntoSessions( logger.verbose(`Created ${sessionScoreGroups.length} groups from timestamped scores.`); - const sessionInfoReturns: SessionInfoReturn[] = []; + const sessionInfoReturns: Array = []; // All async operations inside here *need* to be done in lockstep to avoid colliding sessions. // realistically, that shouldn't be possible, but hey. @@ -213,6 +216,7 @@ export async function LoadScoresIntoSessions( }); const pbMap: Map = new Map(); + for (const pb of pbs) { pbMap.set(pb.chartID, pb); } diff --git a/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.ts b/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.ts index 90fb3aefb..ab4f08f5f 100644 --- a/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.ts +++ b/server/src/lib/score-import/framework/user-game-stats/builtin-class-handlers.ts @@ -6,9 +6,9 @@ import { SDVXVFClasses, WACCA_COLOURS, } from "lib/constants/classes"; -import { KtLogger } from "lib/logger/logger"; -import { Game, integer, Playtype, ScoreCalculatedDataLookup } from "tachi-common"; -import { GameClasses } from "tachi-common/js/game-classes"; +import type { KtLogger } from "lib/logger/logger"; +import type { Game, integer, Playtype, ScoreCalculatedDataLookup } from "tachi-common"; +import type { GameClasses } from "tachi-common/js/game-classes"; export function CalculateSDVXClass( game: Game, 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 f51f1124c..6e80eadfb 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 @@ -1,18 +1,3 @@ -import deepmerge from "deepmerge"; -import db from "external/mongo/db"; -import { KtLogger } from "lib/logger/logger"; -import { EmitWebhookEvent } from "lib/webhooks/webhooks"; -import { - ClassDelta, - Game, - IDStrings, - integer, - Playtype, - Playtypes, - UserGameStats, -} from "tachi-common"; -import { GameClasses } from "tachi-common/js/game-classes"; -import { ReturnClassIfGreater } from "utils/class"; import { CalculateChunithmColour, CalculateGitadoraColour, @@ -21,7 +6,22 @@ import { CalculateSDVXClass, CalculateWACCAColour, } from "./builtin-class-handlers"; -import { ClassHandler, ScoreClasses } from "./types"; +import deepmerge from "deepmerge"; +import db from "external/mongo/db"; +import { EmitWebhookEvent } from "lib/webhooks/webhooks"; +import { ReturnClassIfGreater } from "utils/class"; +import type { ClassHandler, ScoreClasses } from "./types"; +import type { KtLogger } from "lib/logger/logger"; +import type { + ClassDelta, + Game, + IDStrings, + integer, + Playtype, + Playtypes, + UserGameStats, +} from "tachi-common"; +import type { GameClasses } from "tachi-common/js/game-classes"; type ClassHandlerMap = { [G in Game]: @@ -136,8 +136,8 @@ export async function ProcessClassDeltas( userGameStats: UserGameStats | null, userID: integer, logger: KtLogger -): Promise { - const deltas: ClassDelta[] = []; +): Promise> { + const deltas: Array = []; const achievementOps = []; @@ -157,6 +157,7 @@ export async function ProcessClassDeltas( continue; } else { let delta: ClassDelta; + if (isGreater === null) { delta = { game, 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 14ff5e70d..4318dd362 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 @@ -1,6 +1,6 @@ import db from "external/mongo/db"; -import { KtLogger } from "lib/logger/logger"; -import { +import type { KtLogger } from "lib/logger/logger"; +import type { Game, IDStrings, integer, @@ -16,9 +16,9 @@ type CustomCalcNames = ScoreCalculatedDataLookup[IDStrings]; function LazySumAll(key: CustomCalcNames) { return async (game: Game, playtype: Playtype, userID: integer) => { const sc = await db["personal-bests"].find({ - game: game, - playtype: playtype, - userID: userID, + game, + playtype, + userID, isPrimary: true, [`calculatedData.${key}`]: { $type: "number" }, }); @@ -53,9 +53,9 @@ function LazyCalcN( return async (game: Game, playtype: Playtype, userID: integer) => { const sc = await db["personal-bests"].find( { - game: game, - playtype: playtype, - userID: userID, + game, + playtype, + userID, isPrimary: true, [`calculatedData.${key}`]: { $type: "number" }, }, @@ -291,20 +291,21 @@ async function CalculateGitadoraSkill( } let skill = 0; - skill += bestHotScores.reduce((a, e) => a + e.calculatedData.skill!, 0); - skill += bestScores.reduce((a, e) => a + e.calculatedData.skill!, 0); + + skill = skill + bestHotScores.reduce((a, e) => a + e.calculatedData.skill!, 0); + skill = skill + bestScores.reduce((a, e) => a + e.calculatedData.skill!, 0); return skill; } export async function GetBestRatingOnSongs( - songIDs: integer[], + songIDs: Array, userID: integer, game: Game, playtype: Playtype, ratingProp: "skill", limit = 25 -): Promise { +): Promise> { const r = await db["personal-bests"].aggregate([ { $match: { @@ -373,8 +374,9 @@ async function CalculateJubility( ]); let skill = 0; - skill += bestHotScores.reduce((a, e) => a + e.calculatedData.jubility!, 0); - skill += bestScores.reduce((a, e) => a + e.calculatedData.jubility!, 0); + + skill = skill + bestHotScores.reduce((a, e) => a + e.calculatedData.jubility!, 0); + skill = skill + bestScores.reduce((a, e) => a + e.calculatedData.jubility!, 0); return skill; } diff --git a/server/src/lib/score-import/framework/user-game-stats/types.ts b/server/src/lib/score-import/framework/user-game-stats/types.ts index 9b8085f63..6f224f9c0 100644 --- a/server/src/lib/score-import/framework/user-game-stats/types.ts +++ b/server/src/lib/score-import/framework/user-game-stats/types.ts @@ -1,15 +1,13 @@ -import { KtLogger } from "lib/logger/logger"; -import { Game, IDStrings, integer, Playtype } from "tachi-common"; -import { GameClasses } from "tachi-common/js/game-classes"; +import type { KtLogger } from "lib/logger/logger"; +import type { Game, IDStrings, integer, Playtype } from "tachi-common"; +import type { GameClasses } from "tachi-common/js/game-classes"; export type ScoreClasses = Partial>; -export interface ClassHandler { - ( - game: Game, - playtype: Playtype, - userID: integer, - ratings: Record, - logger: KtLogger - ): Promise | ScoreClasses | undefined; -} +export type ClassHandler = ( + game: Game, + playtype: Playtype, + userID: integer, + ratings: Record, + logger: KtLogger +) => Promise | ScoreClasses | undefined; diff --git a/server/src/lib/score-import/framework/user-game-stats/update-ugs.ts b/server/src/lib/score-import/framework/user-game-stats/update-ugs.ts index a497b9189..6b0033297 100644 --- a/server/src/lib/score-import/framework/user-game-stats/update-ugs.ts +++ b/server/src/lib/score-import/framework/user-game-stats/update-ugs.ts @@ -2,13 +2,13 @@ // Some games have dedicated methods to calculate statistics like these, other games do not. // That's about all there is to it! -import db from "external/mongo/db"; -import { CreateGameSettings } from "lib/game-settings/create-game-settings"; -import { KtLogger } from "lib/logger/logger"; -import { ClassDelta, Game, integer, Playtype, UserGameStats } from "tachi-common"; import { ProcessClassDeltas, UpdateUGSClasses } from "./classes"; import { CalculateRatings } from "./rating"; -import { ClassHandler } from "./types"; +import db from "external/mongo/db"; +import { CreateGameSettings } from "lib/game-settings/create-game-settings"; +import type { ClassHandler } from "./types"; +import type { KtLogger } from "lib/logger/logger"; +import type { ClassDelta, Game, integer, Playtype, UserGameStats } from "tachi-common"; export async function UpdateUsersGamePlaytypeStats( game: Game, @@ -16,7 +16,7 @@ export async function UpdateUsersGamePlaytypeStats( userID: integer, classHandler: ClassHandler | null, logger: KtLogger -): Promise { +): Promise> { const ratings = await CalculateRatings(game, playtype, userID, logger); // Attempt to find a users game stats if one already exists. If one doesn't exist, 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 49d4bb93c..6bd70ea6f 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 @@ -3,7 +3,7 @@ import { ServerConfig } from "lib/setup/config"; import nodeFetch from "utils/fetch"; import { HasOwnProperty } from "utils/misc"; import { CreateURLWithParams } from "utils/url"; -import { ClassHandler } from "../../../framework/user-game-stats/types"; +import type { ClassHandler } from "../../../framework/user-game-stats/types"; export async function CreateArcIIDXClassHandler( profileID: string, @@ -44,7 +44,7 @@ export async function CreateArcIIDXClassHandler( // 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 | undefined | null; + let arcClass: string | null | undefined; if (playtype === "SP") { arcClass = json?._items?.[0]?.sp?.rank; diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/converter.ts b/server/src/lib/score-import/import-types/api/arc-iidx/converter.ts index 8d2eee949..7dc43b71b 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/converter.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/converter.ts @@ -1,10 +1,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import p, { PrudenceSchema } from "prudence"; -import { Lamps } from "tachi-common"; -import { FormatPrError } from "utils/prudence"; -import { FindChartOnARCID } from "utils/queries/charts"; -import { FindSongOnIDGuaranteed } from "utils/queries/songs"; -import { EmptyObject } from "utils/types"; import { InvalidScoreFailure, KTDataNotFoundFailure, @@ -13,9 +7,16 @@ import { GenericGetGradeAndPercent, ParseDateFromString, } from "../../../framework/common/score-utils"; -import { DryScore } from "../../../framework/common/types"; -import { ConverterFunction } from "../../common/types"; -import { ARCIIDXScore } from "./types"; +import p from "prudence"; +import { FormatPrError } from "utils/prudence"; +import { FindChartOnARCID } from "utils/queries/charts"; +import { FindSongOnIDGuaranteed } from "utils/queries/songs"; +import type { DryScore } from "../../../framework/common/types"; +import type { ConverterFunction } from "../../common/types"; +import type { ARCIIDXScore } from "./types"; +import type { PrudenceSchema } from "prudence"; +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_ArcIIDXScore: PrudenceSchema = { @@ -50,6 +51,7 @@ export const ConvertAPIArcIIDX: ConverterFunction = async // Remove internal properties. delete (data as any)._links; delete (data as any)._etag; + // Most importantly, this one will cause crashes. delete (data as any)._id; @@ -75,7 +77,7 @@ export const ConvertAPIArcIIDX: ConverterFunction = async const lamp = ResolveARCIIDXLamp(score.lamp); - const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { + const dryScore: DryScore<"iidx:DP" | "iidx:SP"> = { comment: null, game: "iidx", importType, @@ -97,7 +99,7 @@ export const ConvertAPIArcIIDX: ConverterFunction = async return { song, chart, dryScore }; }; -export function ResolveARCIIDXLamp(lamp: ARCIIDXScore["lamp"]): Lamps["iidx:SP" | "iidx:DP"] { +export function ResolveARCIIDXLamp(lamp: ARCIIDXScore["lamp"]): Lamps["iidx:DP" | "iidx:SP"] { switch (lamp) { case "NO_PLAY": return "NO PLAY"; diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/parser.ts b/server/src/lib/score-import/import-types/api/arc-iidx/parser.ts index 26dc96448..ce6e801e6 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/parser.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/parser.ts @@ -1,12 +1,12 @@ -import { KtLogger } from "lib/logger/logger"; +import { CreateArcIIDXClassHandler } from "./class-handler"; +import { TraverseKaiAPI } from "../../common/api-kai/traverse-api"; import { ServerConfig } from "lib/setup/config"; -import { integer } from "tachi-common"; import nodeFetch from "utils/fetch"; import { GetArcAuthGuaranteed } from "utils/queries/auth"; -import { EmptyObject } from "utils/types"; -import { TraverseKaiAPI } from "../../common/api-kai/traverse-api"; -import { ParserFunctionReturns } from "../../common/types"; -import { CreateArcIIDXClassHandler } from "./class-handler"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { KtLogger } from "lib/logger/logger"; +import type { integer } from "tachi-common"; +import type { EmptyObject } from "utils/types"; export async function ParseArcIIDX( userID: integer, @@ -24,6 +24,7 @@ export async function ParseArcIIDX( return { iterable: TraverseKaiAPI( ServerConfig.ARC_API_URL, + // BISTROVER // The trailing slash is critical! ARC only allows /? queries. `/api/v1/iidx/28/player_bests/?profile_id=${authDoc.accountID}`, diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/types.ts b/server/src/lib/score-import/import-types/api/arc-iidx/types.ts index a6a17a12a..896ad68a6 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/types.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/types.ts @@ -1,16 +1,16 @@ -import { integer } from "tachi-common"; +import type { integer } from "tachi-common"; export interface ARCIIDXScore { chart_id: string; lamp: - | "NO_PLAY" - | "FAILED" | "ASSIST_CLEAR" - | "EASY_CLEAR" | "CLEAR" - | "HARD_CLEAR" + | "EASY_CLEAR" | "EX_HARD_CLEAR" - | "FULL_COMBO"; + | "FAILED" + | "FULL_COMBO" + | "HARD_CLEAR" + | "NO_PLAY"; ex_score: integer; miss_count: integer | null; timestamp: string; 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 d43a477e9..7f77648c8 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 @@ -1,9 +1,3 @@ -import p, { PrudenceSchema } from "prudence"; -import { Lamps } from "tachi-common"; -import { FormatPrError } from "utils/prudence"; -import { FindChartOnARCID } from "utils/queries/charts"; -import { FindSongOnIDGuaranteed } from "utils/queries/songs"; -import { EmptyObject } from "utils/types"; import { InvalidScoreFailure, KTDataNotFoundFailure, @@ -12,9 +6,16 @@ import { GenericGetGradeAndPercent, ParseDateFromString, } from "../../../framework/common/score-utils"; -import { DryScore } from "../../../framework/common/types"; -import { ConverterFunction } from "../../common/types"; -import { ARCSDVXScore } from "./types"; +import p from "prudence"; +import { FormatPrError } from "utils/prudence"; +import { FindChartOnARCID } from "utils/queries/charts"; +import { FindSongOnIDGuaranteed } from "utils/queries/songs"; +import type { DryScore } from "../../../framework/common/types"; +import type { ConverterFunction } from "../../common/types"; +import type { ARCSDVXScore } from "./types"; +import type { PrudenceSchema } from "prudence"; +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 = { diff --git a/server/src/lib/score-import/import-types/api/arc-sdvx/parser.ts b/server/src/lib/score-import/import-types/api/arc-sdvx/parser.ts index 8f067ae79..93bd7f8f0 100644 --- a/server/src/lib/score-import/import-types/api/arc-sdvx/parser.ts +++ b/server/src/lib/score-import/import-types/api/arc-sdvx/parser.ts @@ -1,11 +1,11 @@ -import { KtLogger } from "lib/logger/logger"; +import { TraverseKaiAPI } from "../../common/api-kai/traverse-api"; import { ServerConfig } from "lib/setup/config"; -import { integer } from "tachi-common"; import nodeFetch from "utils/fetch"; import { GetArcAuthGuaranteed } from "utils/queries/auth"; -import { EmptyObject } from "utils/types"; -import { TraverseKaiAPI } from "../../common/api-kai/traverse-api"; -import { ParserFunctionReturns } from "../../common/types"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { KtLogger } from "lib/logger/logger"; +import type { integer } from "tachi-common"; +import type { EmptyObject } from "utils/types"; export async function ParseArcSDVX( userID: integer, @@ -23,6 +23,7 @@ export async function ParseArcSDVX( return { iterable: TraverseKaiAPI( ServerConfig.ARC_API_URL, + // VIVID WAVE. // The trailing slash is critical! ARC only allows /? queries. `/api/v1/sdvx/5/player_bests/?profile_id=${authDoc.accountID}`, diff --git a/server/src/lib/score-import/import-types/api/arc-sdvx/types.ts b/server/src/lib/score-import/import-types/api/arc-sdvx/types.ts index 14928d5c6..d419c7d59 100644 --- a/server/src/lib/score-import/import-types/api/arc-sdvx/types.ts +++ b/server/src/lib/score-import/import-types/api/arc-sdvx/types.ts @@ -1,9 +1,9 @@ -import { integer } from "tachi-common"; +import type { integer } from "tachi-common"; export interface ARCSDVXScore { chart_id: string; score: integer; - lamp: "CLEAR" | "UC" | "PUC" | "PLAY" | "HC"; + lamp: "CLEAR" | "HC" | "PLAY" | "PUC" | "UC"; btn_rate: number; long_rate: number; vol_rate: number; diff --git a/server/src/lib/score-import/import-types/api/eag-iidx/parser.ts b/server/src/lib/score-import/import-types/api/eag-iidx/parser.ts index 1758ff18d..20da61482 100644 --- a/server/src/lib/score-import/import-types/api/eag-iidx/parser.ts +++ b/server/src/lib/score-import/import-types/api/eag-iidx/parser.ts @@ -1,7 +1,7 @@ -import { KtLogger } from "lib/logger/logger"; -import { integer } from "tachi-common"; -import { GetKaiAuthGuaranteed } from "utils/queries/auth"; import { ParseKaiIIDX } from "../../common/api-kai/iidx/parser"; +import { GetKaiAuthGuaranteed } from "utils/queries/auth"; +import type { KtLogger } from "lib/logger/logger"; +import type { integer } from "tachi-common"; export async function ParseEagIIDX(userID: integer, logger: KtLogger) { const authDoc = await GetKaiAuthGuaranteed(userID, "EAG", logger); diff --git a/server/src/lib/score-import/import-types/api/eag-sdvx/parser.ts b/server/src/lib/score-import/import-types/api/eag-sdvx/parser.ts index cb3379d4e..e2085a2e1 100644 --- a/server/src/lib/score-import/import-types/api/eag-sdvx/parser.ts +++ b/server/src/lib/score-import/import-types/api/eag-sdvx/parser.ts @@ -1,7 +1,7 @@ -import { KtLogger } from "lib/logger/logger"; -import { integer } from "tachi-common"; -import { GetKaiAuthGuaranteed } from "utils/queries/auth"; import { ParseKaiSDVX } from "../../common/api-kai/sdvx/parser"; +import { GetKaiAuthGuaranteed } from "utils/queries/auth"; +import type { KtLogger } from "lib/logger/logger"; +import type { integer } from "tachi-common"; export async function ParseEagSDVX(userID: integer, logger: KtLogger) { const authDoc = await GetKaiAuthGuaranteed(userID, "EAG", logger); diff --git a/server/src/lib/score-import/import-types/api/flo-iidx/parser.ts b/server/src/lib/score-import/import-types/api/flo-iidx/parser.ts index cef49e74d..45eb57ddb 100644 --- a/server/src/lib/score-import/import-types/api/flo-iidx/parser.ts +++ b/server/src/lib/score-import/import-types/api/flo-iidx/parser.ts @@ -1,7 +1,7 @@ -import { KtLogger } from "lib/logger/logger"; import { ParseKaiIIDX } from "../../common/api-kai/iidx/parser"; -import { integer } from "tachi-common"; import { GetKaiAuthGuaranteed } from "utils/queries/auth"; +import type { KtLogger } from "lib/logger/logger"; +import type { integer } from "tachi-common"; export async function ParseFloIIDX(userID: integer, logger: KtLogger) { const authDoc = await GetKaiAuthGuaranteed(userID, "FLO", logger); diff --git a/server/src/lib/score-import/import-types/api/flo-sdvx/parser.ts b/server/src/lib/score-import/import-types/api/flo-sdvx/parser.ts index def28c295..9f8d83de5 100644 --- a/server/src/lib/score-import/import-types/api/flo-sdvx/parser.ts +++ b/server/src/lib/score-import/import-types/api/flo-sdvx/parser.ts @@ -1,7 +1,7 @@ -import { KtLogger } from "lib/logger/logger"; -import { integer } from "tachi-common"; -import { GetKaiAuthGuaranteed } from "utils/queries/auth"; import { ParseKaiSDVX } from "../../common/api-kai/sdvx/parser"; +import { GetKaiAuthGuaranteed } from "utils/queries/auth"; +import type { KtLogger } from "lib/logger/logger"; +import type { integer } from "tachi-common"; export async function ParseFloSDVX(userID: integer, logger: KtLogger) { const authDoc = await GetKaiAuthGuaranteed(userID, "FLO", logger); diff --git a/server/src/lib/score-import/import-types/api/min-sdvx/parser.ts b/server/src/lib/score-import/import-types/api/min-sdvx/parser.ts index 253d56cc7..6d7ddf6da 100644 --- a/server/src/lib/score-import/import-types/api/min-sdvx/parser.ts +++ b/server/src/lib/score-import/import-types/api/min-sdvx/parser.ts @@ -1,7 +1,7 @@ -import { KtLogger } from "lib/logger/logger"; -import { integer } from "tachi-common"; -import { GetKaiAuthGuaranteed } from "utils/queries/auth"; import { ParseKaiSDVX } from "../../common/api-kai/sdvx/parser"; +import { GetKaiAuthGuaranteed } from "utils/queries/auth"; +import type { KtLogger } from "lib/logger/logger"; +import type { integer } from "tachi-common"; export async function ParseMinSDVX(userID: integer, logger: KtLogger) { const authDoc = await GetKaiAuthGuaranteed(userID, "MIN", logger); 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 17627caaf..648ee9f14 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,10 +1,10 @@ -import { IIDXDans } from "lib/constants/classes"; -import { ClassHandler } from "lib/score-import/framework/user-game-stats/types"; -import nodeFetch from "utils/fetch"; import { KaiTypeToBaseURL } from "../utils"; +import { IIDXDans } from "lib/constants/classes"; +import nodeFetch from "utils/fetch"; +import type { ClassHandler } from "lib/score-import/framework/user-game-stats/types"; export async function CreateKaiIIDXClassHandler( - kaiType: "FLO" | "EAG", + kaiType: "EAG" | "FLO", token: string, fetch = nodeFetch ): Promise { 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 f5a5dbc43..e37801232 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 @@ -1,8 +1,3 @@ -import p from "prudence"; -import { integer, Lamps } from "tachi-common"; -import { FormatPrError } from "utils/prudence"; -import { FindIIDXChartOnInGameIDVersion } from "utils/queries/charts"; -import { FindSongOnID } from "utils/queries/songs"; import { InternalFailure, InvalidScoreFailure, @@ -12,9 +7,14 @@ import { GenericGetGradeAndPercent, ParseDateFromString, } from "../../../../framework/common/score-utils"; -import { DryScore } from "../../../../framework/common/types"; -import { ConverterFunction } from "../../types"; -import { KaiContext, KaiIIDXScore } from "../types"; +import p from "prudence"; +import { FormatPrError } from "utils/prudence"; +import { FindIIDXChartOnInGameIDVersion } from "utils/queries/charts"; +import { FindSongOnID } from "utils/queries/songs"; +import type { DryScore } from "../../../../framework/common/types"; +import type { ConverterFunction } from "../../types"; +import type { KaiContext, KaiIIDXScore } from "../types"; +import type { integer, Lamps } from "tachi-common"; const PR_KaiIIDXScore = { music_id: p.isPositiveInteger, @@ -29,7 +29,7 @@ const PR_KaiIIDXScore = { timestamp: "string", }; -function ResolveKaiLamp(lamp: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7): Lamps["iidx:SP" | "iidx:DP"] { +function ResolveKaiLamp(lamp: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7): Lamps["iidx:DP" | "iidx:SP"] { switch (lamp) { case 0: return "NO PLAY"; @@ -56,94 +56,139 @@ function ResolveKaiLamp(lamp: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7): Lamps["iidx:SP" | const OldLeggendariaConversionTable: Record = { // RUGGED ASH† -> RUGGED ASH 1100: 1017, + // Clione† -> Clione 4100: 4005, + // ABSOLUTE† -> ABSOLUTE 4101: 4001, + // RIDE ON THE LIGHT (HI GREAT MIX) † -> RIDE ON THE LIGHT (HI GREAT MIX) 5100: 5014, + // RED ZONE† -> RED ZONE 11100: 11032, + // spiral galaxy† -> spiral galaxy 11101: 11012, + // Little Little Princess† -> Little Little Princess 12100: 12002, + // CONTRACT† -> CONTRACT 13100: 13010, + // VANESSA† -> VANESSA 14100: 14009, + // KAMAITACHI† -> KAMAITACHI 14101: 14046, + // ICARUS† -> ICARUS 15101: 15023, + // THE DEEP STRIKER† -> THE DEEP STRIKER 15102: 15007, + // Blue Rain† -> Blue Rain 15104: 15004, + // Wanna Party?† -> Wanna Party? 15105: 15045, + // 凛として咲く花の如く† -> 凛として咲く花の如く 16101: 16050, + // THANK YOU FOR PLAYING† -> THANK YOU FOR PLAYING 16102: 16045, + // naughty girl@Queen's Palace† -> naughty girl@Queen's Palace 16103: 16031, + // Kung-fu Empire† -> Kung-fu Empire 16104: 16015, + // SOLID STATE SQUAD† -> SOLID STATE SQUAD 17101: 17060, + // Golden Palms† -> Golden Palms 18100: 18025, + // おおきなこえで† -> おおきなこえで 18103: 18011, + // QUANTUM TELEPORTATION† -> QUANTUM TELEPORTATION 19100: 19063, + // 朧† -> 朧 20103: 20100, + // 仮想空間の旅人たち† -> 仮想空間の旅人たち 20104: 20039, + // LUV CAN SAVE U† -> LUV CAN SAVE U 20105: 20068, + // Howling† -> Howling 20106: 20024, + // 龍と少女とデコヒーレンス† -> 龍と少女とデコヒーレンス 20107: 20019, + // Close the World feat.a☆ru†LEGGENDARIA -> Close the World feat. a☆ru 21100: 21012, + // Sigmund†LEGGENDARIA -> Sigmund 21101: 21059, + // Ancient Scapes†LEGGENDARIA -> Ancient Scapes 21102: 21069, + // invoker†LEGGENDARIA -> invoker 21103: 21073, + // Feel The Beat†LEGGENDARIA -> Feel The Beat 21104: 21052, + // 疾風迅雷†LEGGENDARIA -> 疾風迅雷 21105: 21048, + // Verflucht†LEGGENDARIA -> Verflucht 21106: 21050, + // 廿† -> 廿 21107: 21029, + // CHRONO DIVER -NORNIR-† -> CHRONO DIVER -NORNIR- 22101: 22008, + // chrono diver -fragment-† -> chrono diver -fragment- 22102: 22013, + // 恋は白帯、サンシロー† -> 恋は白帯、サンシロー 22103: 22024, + // Beat Radiance† -> Beat Radiance 22104: 22027, + // 超青少年ノ為ノ超多幸ナ超古典的超舞曲† -> 超青少年ノ為ノ超多幸ナ超古典的超舞曲 22105: 22031, + // EBONY & IVORY† -> EBONY & IVORY 22106: 22089, + // Cosmic Cat† -> Cosmic Cat 22107: 22006, + // Damage Per Second† -> Damage Per Second 23100: 23054, + // STARLIGHT DANCEHALL† -> STARLIGHT DANCEHALL 23101: 23031, + // Amazing Mirage† -> Amazing Mirage 24100: 24041, + // 冬椿 ft. Kanae Asaba† -> 冬椿 ft. Kanae Asaba 24101: 24011, }; @@ -184,6 +229,7 @@ export const ConvertAPIKaiIIDX: ConverterFunction = async ( musicID, playtype, score.difficulty, + // they send integers like 25, 27 - this will convert to our versions. version as "20" | "21" | "22" | "23" | "24" | "25" | "26" | "27" | "28" ); @@ -210,7 +256,7 @@ export const ConvertAPIKaiIIDX: ConverterFunction = async ( const timeAchieved = ParseDateFromString(score.timestamp); - const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { + const dryScore: DryScore<"iidx:DP" | "iidx:SP"> = { comment: null, game: "iidx", importType, diff --git a/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.ts b/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.ts index 3d2ea28e0..12fce6a53 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/iidx/parser.ts @@ -1,15 +1,15 @@ -import { KtLogger } from "lib/logger/logger"; -import { KaiAuthDocument } from "tachi-common"; -import nodeFetch from "utils/fetch"; -import { ParserFunctionReturns } from "../../types"; +import { CreateKaiIIDXClassHandler } from "./class-handler"; import { CreateKaiReauthFunction } from "../reauth"; import { TraverseKaiAPI } from "../traverse-api"; -import { KaiContext } from "../types"; import { KaiTypeToBaseURL } from "../utils"; -import { CreateKaiIIDXClassHandler } from "./class-handler"; +import nodeFetch from "utils/fetch"; +import type { ParserFunctionReturns } from "../../types"; +import type { KaiContext } from "../types"; +import type { KtLogger } from "lib/logger/logger"; +import type { KaiAuthDocument } from "tachi-common"; export async function ParseKaiIIDX( - service: "FLO" | "EAG", + service: "EAG" | "FLO", authDoc: KaiAuthDocument, logger: KtLogger, fetch = nodeFetch 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 d5de63e6b..dfc268806 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 @@ -1,11 +1,11 @@ +import { GetKaiTypeClientCredentials, KaiTypeToBaseURL } from "./utils"; import db from "external/mongo/db"; -import { KtLogger } from "lib/logger/logger"; import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error"; import p from "prudence"; -import { KaiAuthDocument } from "tachi-common"; import nodeFetch from "utils/fetch"; import { CreateURLWithParams } from "utils/url"; -import { GetKaiTypeClientCredentials, KaiTypeToBaseURL } from "./utils"; +import type { KtLogger } from "lib/logger/logger"; +import type { KaiAuthDocument } from "tachi-common"; const REAUTH_SCHEMA = { access_token: "string", @@ -13,7 +13,7 @@ const REAUTH_SCHEMA = { }; export function CreateKaiReauthFunction( - kaiType: "FLO" | "EAG" | "MIN", + kaiType: "EAG" | "FLO" | "MIN", authDoc: KaiAuthDocument, logger: KtLogger, fetch = nodeFetch @@ -35,6 +35,7 @@ export function CreateKaiReauthFunction( return async () => { let res; + try { const url = CreateURLWithParams(`${KaiTypeToBaseURL(kaiType)}/oauth/token`, { refresh_token: authDoc.refreshToken, @@ -63,6 +64,7 @@ export function CreateKaiReauthFunction( let json; /* istanbul ignore next */ + try { json = await res.json(); } catch (err) { 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 f3c1982ac..c9e7cf325 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,10 +1,10 @@ -import { SDVXDans } from "lib/constants/classes"; -import { ClassHandler } from "lib/score-import/framework/user-game-stats/types"; -import nodeFetch from "utils/fetch"; import { KaiTypeToBaseURL } from "../utils"; +import { SDVXDans } from "lib/constants/classes"; +import nodeFetch from "utils/fetch"; +import type { ClassHandler } from "lib/score-import/framework/user-game-stats/types"; export async function CreateKaiSDVXClassHandler( - kaiType: "FLO" | "EAG" | "MIN", + kaiType: "EAG" | "FLO" | "MIN", token: string, fetch = nodeFetch ): Promise { 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 84805e5ff..c3050eab2 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 @@ -1,8 +1,3 @@ -import p from "prudence"; -import { Lamps } from "tachi-common"; -import { FormatPrError } from "utils/prudence"; -import { FindSDVXChartOnInGameIDVersion } from "utils/queries/charts"; -import { FindSongOnID } from "utils/queries/songs"; import { InternalFailure, InvalidScoreFailure, @@ -12,9 +7,14 @@ import { GenericGetGradeAndPercent, ParseDateFromString, } from "../../../../framework/common/score-utils"; -import { DryScore } from "../../../../framework/common/types"; -import { ConverterFunction } from "../../types"; -import { KaiContext, KaiSDVXScore } from "../types"; +import p from "prudence"; +import { FormatPrError } from "utils/prudence"; +import { FindSDVXChartOnInGameIDVersion } from "utils/queries/charts"; +import { FindSongOnID } from "utils/queries/songs"; +import type { DryScore } from "../../../../framework/common/types"; +import type { ConverterFunction } from "../../types"; +import type { KaiContext, KaiSDVXScore } from "../types"; +import type { Lamps } from "tachi-common"; const PR_KaiSDVXScore = { music_id: p.isPositiveInteger, diff --git a/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.ts b/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.ts index fbd37aa5c..e34a28edf 100644 --- a/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.ts +++ b/server/src/lib/score-import/import-types/common/api-kai/sdvx/parser.ts @@ -1,15 +1,15 @@ -import { KtLogger } from "lib/logger/logger"; -import { KaiAuthDocument } from "tachi-common"; -import nodeFetch from "utils/fetch"; -import { ParserFunctionReturns } from "../../types"; +import { CreateKaiSDVXClassHandler } from "./class-handler"; import { CreateKaiReauthFunction } from "../reauth"; import { TraverseKaiAPI } from "../traverse-api"; -import { KaiContext } from "../types"; import { KaiTypeToBaseURL } from "../utils"; -import { CreateKaiSDVXClassHandler } from "./class-handler"; +import nodeFetch from "utils/fetch"; +import type { ParserFunctionReturns } from "../../types"; +import type { KaiContext } from "../types"; +import type { KtLogger } from "lib/logger/logger"; +import type { KaiAuthDocument } from "tachi-common"; export async function ParseKaiSDVX( - service: "FLO" | "EAG" | "MIN", + service: "EAG" | "FLO" | "MIN", authDoc: KaiAuthDocument, logger: KtLogger, fetch = nodeFetch 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 2a6bd8469..6b7ac732c 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 @@ -1,9 +1,9 @@ -import { VERSION_STR } from "lib/constants/version"; -import { KtLogger } from "lib/logger/logger"; -import { TachiConfig } from "lib/setup/config"; -import { URL } from "url"; -import nodeFetch from "utils/fetch"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; +import { VERSION_STR } from "lib/constants/version"; +import { TachiConfig } from "lib/setup/config"; +import nodeFetch from "utils/fetch"; +import { URL } from "url"; +import type { KtLogger } from "lib/logger/logger"; /** * A Kai Reauth function is an async function that returns a string @@ -64,8 +64,10 @@ export async function* TraverseKaiAPI( let json; let res; + // wrap all this in a try catch just incase the fetch or the res // json call fails. + try { // eslint-disable-next-line no-await-in-loop res = await fetch(url, { 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 54dceb86f..cecbd4be9 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 @@ -1,9 +1,9 @@ -import { integer } from "tachi-common"; +import type { integer } from "tachi-common"; export interface KaiIIDXScore { music_id: integer; - play_style: "SINGLE" | "DOUBLE"; - difficulty: "BEGINNER" | "NORMAL" | "HYPER" | "ANOTHER" | "LEGGENDARIA"; + play_style: "DOUBLE" | "SINGLE"; + difficulty: "ANOTHER" | "BEGINNER" | "HYPER" | "LEGGENDARIA" | "NORMAL"; version_played: integer; lamp: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; ex_score: integer; @@ -31,5 +31,5 @@ export interface KaiSDVXScore { } export interface KaiContext { - service: "FLO" | "EAG" | "MIN"; + service: "EAG" | "FLO" | "MIN"; } 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 0dc40310c..302095f21 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,30 @@ import { ServerConfig } from "lib/setup/config"; -export function KaiTypeToBaseURL(kaiType: "FLO" | "EAG" | "MIN") { +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?`); } + 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?`); } + 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?`); } + return ServerConfig.MIN_API_URL; } throw new Error(`Invalid Kai Type ${kaiType} provided.`); } -export function GetKaiTypeClientCredentials(kaiType: "FLO" | "EAG" | "MIN") { +export function GetKaiTypeClientCredentials(kaiType: "EAG" | "FLO" | "MIN") { if (kaiType === "FLO") { return ServerConfig.FLO_OAUTH2_INFO; } else if (kaiType === "EAG") { diff --git a/server/src/lib/score-import/import-types/common/batch-manual/class-handler.ts b/server/src/lib/score-import/import-types/common/batch-manual/class-handler.ts index 2b50364c8..3ca548e40 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/class-handler.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/class-handler.ts @@ -1,7 +1,8 @@ import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error"; -import { ClassHandler } from "lib/score-import/framework/user-game-stats/types"; -import { GamePTConfig, GetGamePTConfig, IDStrings } from "tachi-common"; -import { GameClasses, GameClassSets } from "tachi-common/js/game-classes"; +import { GetGamePTConfig } from "tachi-common"; +import type { ClassHandler } from "lib/score-import/framework/user-game-stats/types"; +import type { GamePTConfig, IDStrings } from "tachi-common"; +import type { GameClasses, GameClassSets } from "tachi-common/js/game-classes"; // Note: This is tested by batch-manuals parser.test.ts. export function CreateBatchManualClassHandler( 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 fa5be9fea..765de41db 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 @@ -1,20 +1,3 @@ -import db from "external/mongo/db"; -import { KtLogger } from "lib/logger/logger"; -import { - BatchManualScore, - ChartDocument, - Grades, - IDStrings, - ImportTypes, - SongDocument, -} from "tachi-common"; -import { - FindBMSChartOnHash, - FindChartWithPTDF, - FindChartWithPTDFVersion, - FindDDRChartOnSongHash, -} from "utils/queries/charts"; -import { FindSongOnID, FindSongOnTitleInsensitive } from "utils/queries/songs"; import { InternalFailure, InvalidScoreFailure, @@ -25,9 +8,27 @@ import { AssertStrAsDifficulty, AssertStrAsPositiveInt, } from "../../../framework/common/string-asserts"; -import { DryScore } from "../../../framework/common/types"; -import { ConverterFunction } from "../types"; -import { BatchManualContext } from "./types"; +import db from "external/mongo/db"; +import { + FindBMSChartOnHash, + FindChartWithPTDF, + FindChartWithPTDFVersion, + FindDDRChartOnSongHash, +} from "utils/queries/charts"; +import { FindSongOnID, FindSongOnTitleInsensitive } from "utils/queries/songs"; +import type { DryScore } from "../../../framework/common/types"; +import type { ConverterFunction } from "../types"; +import type { BatchManualContext } from "./types"; +import type { KtLogger } from "lib/logger/logger"; +import type { + BatchManualScore, + ChartDocument, + Grades, + IDStrings, + ImportTypes, + SongDocument, +} from "tachi-common"; + /** * Creates a ConverterFn for the BatchManualScore format. This curries * the importType into the function, so the right failures can be @@ -58,10 +59,7 @@ export const ConverterBatchManual: ConverterFunction 100 && - (chart as ChartDocument<"jubeat:Single">).data.isHardMode === false - ) { + if (data.percent > 100 && !(chart as ChartDocument<"jubeat:Single">).data.isHardMode) { throw new InvalidScoreFailure(`The percent field must be <= 100 for normal mode.`); } @@ -89,13 +87,13 @@ export const ConverterBatchManual: ConverterFunction p.optional(p.nullable(v)); @@ -150,6 +144,7 @@ const PR_HitMeta = (game: Game): PrudenceSchema => { const PR_BatchManualScore = (game: Game, playtype: Playtype): PrudenceSchema => { const gptConfig = GetGamePTConfig(game, playtype); + return { score: p.isPositiveInteger, lamp: p.isIn(gptConfig.lamps), @@ -187,6 +182,7 @@ const PR_BatchManualScore = (game: Game, playtype: Playtype): PrudenceSchema => // @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}]`; } @@ -264,9 +260,10 @@ export function ParseBatchManualFromObject( // attempt to retrieve game // @ts-expect-error man. - const possiblyGame = object?.meta?.game; + const possiblyGame = object.meta?.game; + // @ts-expect-error man. - const possiblyPlaytype = object?.meta?.playtype; + const possiblyPlaytype = object.meta?.playtype; if (!possiblyGame) { throw new ScoreImportFatalError( @@ -326,10 +323,11 @@ export function ParseBatchManualFromObject( version: batchManual.meta.version ?? null, }, iterable: batchManual.scores, + // if classes are provided, use those as a class handler. Otherwise, we // don't care. classHandler: batchManual.classes - ? CreateBatchManualClassHandler(batchManual.classes!) + ? CreateBatchManualClassHandler(batchManual.classes) : null, }; } diff --git a/server/src/lib/score-import/import-types/common/batch-manual/types.ts b/server/src/lib/score-import/import-types/common/batch-manual/types.ts index 8778f6239..971ede2ce 100644 --- a/server/src/lib/score-import/import-types/common/batch-manual/types.ts +++ b/server/src/lib/score-import/import-types/common/batch-manual/types.ts @@ -1,4 +1,4 @@ -import { Game, GPTSupportedVersions, IDStrings, Playtype } from "tachi-common"; +import type { Game, GPTSupportedVersions, IDStrings, Playtype } from "tachi-common"; export interface BatchManualContext { game: 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 d8f7da3b8..f22fbd5a6 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 @@ -1,6 +1,3 @@ -import { ChartDocument, Lamps } from "tachi-common"; -import { FindChartWithPTDFVersion } from "utils/queries/charts"; -import { FindSongOnTitle } from "utils/queries/songs"; import { InvalidScoreFailure, KTDataNotFoundFailure, @@ -11,11 +8,14 @@ import { ParseDateFromString, } from "../../../framework/common/score-utils"; import { AssertStrAsPositiveInt } from "../../../framework/common/string-asserts"; -import { DryScore } from "../../../framework/common/types"; -import { ConverterFunction } from "../types"; -import { IIDXEamusementCSVContext, IIDXEamusementCSVData } from "./types"; +import { FindChartWithPTDFVersion } from "utils/queries/charts"; +import { FindSongOnTitle } from "utils/queries/songs"; +import type { DryScore } from "../../../framework/common/types"; +import type { ConverterFunction } from "../types"; +import type { IIDXEamusementCSVContext, IIDXEamusementCSVData } from "./types"; +import type { ChartDocument, Lamps } from "tachi-common"; -const EAMUSEMENT_LAMP_RESOLVER: Map = new Map([ +const EAMUSEMENT_LAMP_RESOLVER: Map = new Map([ ["NO PLAY", "NO PLAY"], ["FAILED", "FAILED"], ["FULLCOMBO CLEAR", "FULL COMBO"], @@ -50,7 +50,7 @@ const ConvertEamIIDXCSV: ConverterFunction< // if pre-HV, leggendarias were stored in a wacky form. if (!context.hasBeginnerAndLegg) { // hack fix for legacy LEGGENDARIA titles - if (data.title.match(/(†|†LEGGENDARIA)$/u)) { + if (/(†|†LEGGENDARIA)$/u.exec(data.title)) { data.title = data.title.replace(/(†|†LEGGENDARIA)$/u, "").trimEnd(); isLegacyLeggendaria = true; } @@ -82,7 +82,7 @@ const ConvertEamIIDXCSV: ConverterFunction< context.playtype, eamScore.difficulty, context.importVersion - )) as ChartDocument<"iidx:SP" | "iidx:DP">; + )) as ChartDocument<"iidx:DP" | "iidx:SP">; if (!tachiChart) { throw new KTDataNotFoundFailure( @@ -135,7 +135,7 @@ const ConvertEamIIDXCSV: ConverterFunction< const timestamp = ParseDateFromString(data.timestamp); - const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { + const dryScore: DryScore<"iidx:DP" | "iidx:SP"> = { service: context.service, comment: null, game: "iidx", @@ -152,6 +152,7 @@ const ConvertEamIIDXCSV: ConverterFunction< grade, }, scoreMeta: {}, + // japan is gmt+9 timeAchieved: timestamp ? timestamp - NINE_HOURS : null, }; @@ -164,6 +165,7 @@ const ConvertEamIIDXCSV: ConverterFunction< `${HUMANISED_CHART_TITLE} - Invalid BP of ${eamScore.bp}.` ); } + dryScore.scoreData.hitMeta.bp = numBP; } else if (eamScore.bp === "---") { logger.debug( 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 967c6c5d5..a24634db5 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 @@ -1,8 +1,8 @@ -import { KtLogger } from "lib/logger/logger"; -import { CSVParseError, NaiveCSVParse } from "utils/naive-csv-parser"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; -import { ParserFunctionReturns } from "../types"; -import { EamusementScoreData, IIDXEamusementCSVContext, IIDXEamusementCSVData } from "./types"; +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"; enum EAM_VERSION_NAMES { "1st&substream" = 1, @@ -72,7 +72,7 @@ const HV_HEADER_COUNT = 41; // "timestamp", // ]; -export function ResolveHeaders(headers: string[], logger: KtLogger) { +export function ResolveHeaders(headers: Array, logger: KtLogger) { if (headers.length === PRE_HV_HEADER_COUNT) { logger.verbose("PRE_HV csv received."); return { @@ -93,14 +93,16 @@ export function ResolveHeaders(headers: string[], logger: KtLogger) { } export function IIDXCSVParse(csvBuffer: Buffer, logger: KtLogger) { - let rawHeaders: string[]; - let rawRows: string[][]; + let rawHeaders: Array; + let rawRows: Array>; + try { ({ rawHeaders, rawRows } = NaiveCSVParse(csvBuffer, logger)); } catch (e) { if (e instanceof CSVParseError) { throw new ScoreImportFatalError(400, e.message); } + throw e; } @@ -134,7 +136,7 @@ export function IIDXCSVParse(csvBuffer: Buffer, logger: KtLogger) { gameVersion = versionNum; } - const scores: EamusementScoreData[] = []; + const scores: Array = []; for (let d = 0; d < diffs.length; d++) { const diff = diffs[d]; @@ -182,7 +184,7 @@ function GenericParseEamIIDXCSV( service: string, logger: KtLogger ): ParserFunctionReturns { - let playtype: "SP" | "DP"; + let playtype: "DP" | "SP"; if (body.playtype === "SP") { playtype = "SP"; diff --git a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/types.ts b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/types.ts index 8debd8c1c..661dca79c 100644 --- a/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/types.ts +++ b/server/src/lib/score-import/import-types/common/eamusement-iidx-csv/types.ts @@ -1,7 +1,7 @@ -import { GPTSupportedVersions } from "tachi-common"; +import type { GPTSupportedVersions } from "tachi-common"; export interface EamusementScoreData { - difficulty: "BEGINNER" | "NORMAL" | "HYPER" | "ANOTHER" | "LEGGENDARIA"; + difficulty: "ANOTHER" | "BEGINNER" | "HYPER" | "LEGGENDARIA" | "NORMAL"; lamp: string; exscore: string; pgreat: string; @@ -10,26 +10,27 @@ export interface EamusementScoreData { level: string; } -type BaseProps = { +interface BaseProps { title: string; timestamp: string; +} + +export type IIDXEamusementCSVData = BaseProps & { + score: EamusementScoreData; }; -export type IIDXEamusementCSVData = { - score: EamusementScoreData; -} & BaseProps; +type Props = "bp" | "exscore" | "great" | "lamp" | "level" | "pgreat"; -type Props = "exscore" | "lamp" | "pgreat" | "great" | "bp" | "level"; +type RawPropKeys = `${"another" | "beginner" | "hyper" | "leggendaria" | "normal"}-${Props}`; -type RawPropKeys = `${"beginner" | "normal" | "hyper" | "another" | "leggendaria"}-${Props}`; - -export type RawIIDXEamusementCSVData = { - [K in RawPropKeys]: unknown; -} & BaseProps & { [index: string]: unknown }; +export type RawIIDXEamusementCSVData = BaseProps & + Record & { + [K in RawPropKeys]: unknown; + }; export interface IIDXEamusementCSVContext { - playtype: "SP" | "DP"; - importVersion: GPTSupportedVersions["iidx:SP" | "iidx:DP"]; + playtype: "DP" | "SP"; + importVersion: GPTSupportedVersions["iidx:DP" | "iidx:SP"]; hasBeginnerAndLegg: boolean; service: string; } diff --git a/server/src/lib/score-import/import-types/common/types.ts b/server/src/lib/score-import/import-types/common/types.ts index 364bbf680..9de5f1ec6 100644 --- a/server/src/lib/score-import/import-types/common/types.ts +++ b/server/src/lib/score-import/import-types/common/types.ts @@ -1,6 +1,22 @@ -import { KtLogger } from "lib/logger/logger"; -import { USCClientScore } from "server/router/ir/usc/_playtype/types"; -import { +import type { ConverterFailure } from "../../framework/common/converter-failures"; +import type { DryScore } from "../../framework/common/types"; +import type { ClassHandler } from "../../framework/user-game-stats/types"; +import type { SDVXEamusementCSVData } from "../file/eamusement-sdvx-csv/types"; +import type { MerScore } from "../file/mer-iidx/types"; +import type { S3Score } from "../file/solid-state-squad/types"; +import type { BarbatosContext, BarbatosScore } from "../ir/barbatos/types"; +import type { BeatorajaContext, BeatorajaScore } from "../ir/beatoraja/types"; +import type { FervidexStaticContext, FervidexStaticScore } from "../ir/fervidex-static/types"; +import type { FervidexContext, FervidexScore } from "../ir/fervidex/types"; +import type { KsHookSV6CContext, KsHookSV6CScore } from "../ir/kshook-sv6c/types"; +import type { LR2HookContext, LR2HookScore } from "../ir/lr2hook/types"; +import type { IRUSCContext } from "../ir/usc/types"; +import type { KaiContext } from "./api-kai/types"; +import type { BatchManualContext } from "./batch-manual/types"; +import type { IIDXEamusementCSVContext, IIDXEamusementCSVData } from "./eamusement-iidx-csv/types"; +import type { KtLogger } from "lib/logger/logger"; +import type { USCClientScore } from "server/router/ir/usc/_playtype/types"; +import type { BatchManualScore, ChartDocument, Game, @@ -9,23 +25,7 @@ import { MongoDBDocument, SongDocument, } from "tachi-common"; -import { EmptyObject } from "utils/types"; -import { ConverterFailure } from "../../framework/common/converter-failures"; -import { DryScore } from "../../framework/common/types"; -import { ClassHandler } from "../../framework/user-game-stats/types"; -import { SDVXEamusementCSVData } from "../file/eamusement-sdvx-csv/types"; -import { MerScore } from "../file/mer-iidx/types"; -import { S3Score } from "../file/solid-state-squad/types"; -import { BarbatosContext, BarbatosScore } from "../ir/barbatos/types"; -import { BeatorajaContext, BeatorajaScore } from "../ir/beatoraja/types"; -import { FervidexStaticContext, FervidexStaticScore } from "../ir/fervidex-static/types"; -import { FervidexContext, FervidexScore } from "../ir/fervidex/types"; -import { KsHookSV6CContext, KsHookSV6CScore } from "../ir/kshook-sv6c/types"; -import { LR2HookContext, LR2HookScore } from "../ir/lr2hook/types"; -import { IRUSCContext } from "../ir/usc/types"; -import { KaiContext } from "./api-kai/types"; -import { BatchManualContext } from "./batch-manual/types"; -import { IIDXEamusementCSVContext, IIDXEamusementCSVData } from "./eamusement-iidx-csv/types"; +import type { EmptyObject } from "utils/types"; export interface ImportTypeDataMap { "file/eamusement-iidx-csv": IIDXEamusementCSVData; @@ -100,24 +100,24 @@ export interface ConverterFnSuccessReturn { export type ConverterFnReturnOrFailure = ConverterFailure | ConverterFnSuccessReturn; -export interface ConverterFunction { - ( - data: D, - processContext: C, - importType: ImportTypes, - logger: KtLogger - ): Promise; -} +export type ConverterFunction = ( + data: D, + processContext: C, + importType: ImportTypes, + logger: KtLogger +) => Promise; -export interface ImportInputParser { - (logger: KtLogger): ParserFunctionReturns | Promise>; -} +export type ImportInputParser = ( + logger: KtLogger +) => ParserFunctionReturns | Promise>; export interface ParserFunctionReturns { - iterable: Iterable | AsyncIterable; + iterable: AsyncIterable | Iterable; context: C; game: Game; classHandler: ClassHandler | null; } -export type ParserFunction = (...args: A) => ParserFunctionReturns; +export type ParserFunction> = ( + ...args: A +) => ParserFunctionReturns; diff --git a/server/src/lib/score-import/import-types/converters.ts b/server/src/lib/score-import/import-types/converters.ts index 3312ef54f..af7e211ba 100644 --- a/server/src/lib/score-import/import-types/converters.ts +++ b/server/src/lib/score-import/import-types/converters.ts @@ -1,11 +1,9 @@ -import { ImportTypes } from "tachi-common"; import { ConvertAPIArcIIDX } from "./api/arc-iidx/converter"; import { ConvertAPIArcSDVX } from "./api/arc-sdvx/converter"; import { ConvertAPIKaiIIDX } from "./common/api-kai/iidx/converter"; import { ConvertAPIKaiSDVX } from "./common/api-kai/sdvx/converter"; import { ConverterBatchManual } from "./common/batch-manual/converter"; import ConvertEamIIDXCSV from "./common/eamusement-iidx-csv/converter"; -import { ConverterFunction, ImportTypeContextMap, ImportTypeDataMap } from "./common/types"; import ConvertEamSDVXCSV from "./file/eamusement-sdvx-csv/converter"; import { ConvertFileMerIIDX } from "./file/mer-iidx/converter"; import { ConvertFileS3 } from "./file/solid-state-squad/converter"; @@ -16,6 +14,8 @@ import { ConverterIRFervidex } from "./ir/fervidex/converter"; import { ConverterIRKsHookSV6C } from "./ir/kshook-sv6c/converter"; import { ConverterLR2Hook } from "./ir/lr2hook/converter"; import { ConverterIRUSC } from "./ir/usc/converter"; +import type { ConverterFunction, ImportTypeContextMap, ImportTypeDataMap } from "./common/types"; +import type { ImportTypes } from "tachi-common"; export type ConverterMap = { [I in ImportTypes]: ConverterFunction; 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 f8f84cc89..aa9e872e5 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 @@ -1,9 +1,9 @@ -import { KtLogger } from "lib/logger/logger"; -import { BatchManualScore } from "tachi-common"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; import { ParseBatchManualFromObject } from "../../common/batch-manual/parser"; -import { BatchManualContext } from "../../common/batch-manual/types"; -import { ParserFunctionReturns } from "../../common/types"; +import type { BatchManualContext } from "../../common/batch-manual/types"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { KtLogger } from "lib/logger/logger"; +import type { BatchManualScore } from "tachi-common"; /** * Parses a buffer of BATCH-MANUAL data. @@ -22,7 +22,7 @@ function ParseBatchManual( } catch (err) { throw new ScoreImportFatalError( 400, - `Invalid JSON. (${(err as Error)?.message ?? "No Error Message Available."})` + `Invalid JSON. (${(err as Error).message ?? "No Error Message Available."})` ); } diff --git a/server/src/lib/score-import/import-types/file/eamusement-iidx-csv/parser.ts b/server/src/lib/score-import/import-types/file/eamusement-iidx-csv/parser.ts index a6d781208..e6c1abb9d 100644 --- a/server/src/lib/score-import/import-types/file/eamusement-iidx-csv/parser.ts +++ b/server/src/lib/score-import/import-types/file/eamusement-iidx-csv/parser.ts @@ -1,10 +1,10 @@ -import { KtLogger } from "lib/logger/logger"; import GenericParseEamIIDXCSV from "../../common/eamusement-iidx-csv/parser"; -import { +import type { IIDXEamusementCSVContext, IIDXEamusementCSVData, } from "../../common/eamusement-iidx-csv/types"; -import { ParserFunctionReturns } from "../../common/types"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { KtLogger } from "lib/logger/logger"; function ParseEamusementIIDXCSV( fileData: Express.Multer.File, diff --git a/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/converter.ts b/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/converter.ts index 4bfb228ea..e7c301c18 100644 --- a/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/converter.ts +++ b/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/converter.ts @@ -4,13 +4,13 @@ import { } from "lib/score-import/framework/common/converter-failures"; import { GenericGetGradeAndPercent } from "lib/score-import/framework/common/score-utils"; import { AssertStrAsPositiveInt } from "lib/score-import/framework/common/string-asserts"; -import { DryScore } from "lib/score-import/framework/common/types"; -import { Difficulties, Lamps } from "tachi-common"; import { FindChartWithPTDF } from "utils/queries/charts"; import { FindSongOnTitle } from "utils/queries/songs"; -import { EmptyObject } from "utils/types"; -import { ConverterFunction } from "../../common/types"; -import { SDVXEamusementCSVData } from "./types"; +import type { ConverterFunction } from "../../common/types"; +import type { SDVXEamusementCSVData } from "./types"; +import type { DryScore } from "lib/score-import/framework/common/types"; +import type { Difficulties, Lamps } from "tachi-common"; +import type { EmptyObject } from "utils/types"; const DIFFICULTY_MAP: Map = new Map([ ["NOVICE", "NOV"], @@ -98,6 +98,7 @@ const ConvertEamSDVXCSV: ConverterFunction = service: "e-amusement", game: "sdvx", scoreMeta: {}, + // No timestamp data :( timeAchieved: null, comment: null, diff --git a/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/parser.ts b/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/parser.ts index d2583bdd1..ba4ab11c5 100644 --- a/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/parser.ts +++ b/server/src/lib/score-import/import-types/file/eamusement-sdvx-csv/parser.ts @@ -1,9 +1,9 @@ -import { KtLogger } from "lib/logger/logger"; -import { CSVParseError, NaiveCSVParse } from "utils/naive-csv-parser"; -import { EmptyObject } from "utils/types"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; -import { ParserFunctionReturns } from "../../common/types"; -import { SDVXEamusementCSVData } from "./types"; +import { CSVParseError, NaiveCSVParse } from "utils/naive-csv-parser"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { SDVXEamusementCSVData } from "./types"; +import type { KtLogger } from "lib/logger/logger"; +import type { EmptyObject } from "utils/types"; const HEADER_COUNT = 11; @@ -12,14 +12,16 @@ export default function ParseEamusementSDVXCSV( _body: Record, logger: KtLogger ): ParserFunctionReturns { - let rawHeaders: string[]; - let rawRows: string[][]; + let rawHeaders: Array; + let rawRows: Array>; + try { ({ rawHeaders, rawRows } = NaiveCSVParse(fileData.buffer, logger)); } catch (e) { if (e instanceof CSVParseError) { throw new ScoreImportFatalError(400, e.message); } + throw e; } @@ -37,9 +39,11 @@ export default function ParseEamusementSDVXCSV( level: cells[2], lamp: cells[3], score: cells[5], + // @todo exscore is currently unused, but we should eventually store it. // It is 0 if the score was played without S-criticals. exscore: cells[6], + // The other columns (grade, # of different clears) are essentially useless. // There is no timestamp :( })); 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 4410762bb..1a869c75c 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 @@ -1,7 +1,3 @@ -import { Lamps } from "tachi-common"; -import { FindIIDXChartOnInGameID } from "utils/queries/charts"; -import { FindSongOnID } from "utils/queries/songs"; -import { EmptyObject } from "utils/types"; import { InternalFailure, KTDataNotFoundFailure, @@ -10,9 +6,13 @@ import { GenericGetGradeAndPercent, ParseDateFromString, } from "../../../framework/common/score-utils"; -import { DryScore } from "../../../framework/common/types"; -import { ConverterFunction } from "../../common/types"; -import { MerScore } from "./types"; +import { FindIIDXChartOnInGameID } from "utils/queries/charts"; +import { FindSongOnID } from "utils/queries/songs"; +import type { DryScore } from "../../../framework/common/types"; +import type { ConverterFunction } from "../../common/types"; +import type { MerScore } from "./types"; +import type { Lamps } from "tachi-common"; +import type { EmptyObject } from "utils/types"; function ConvertMERLamp(lamp: MerScore["clear_type"]): Lamps["iidx:DP" | "iidx:SP"] { if (lamp === "FULLCOMBO CLEAR") { @@ -54,7 +54,7 @@ export const ConvertFileMerIIDX: ConverterFunction = asyn const timeAchieved = ParseDateFromString(ConvertDateToJST(data.update_time)); - const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { + const dryScore: DryScore<"iidx:DP" | "iidx:SP"> = { game: "iidx", comment: null, importType: "file/mer-iidx", 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 b49ff78ab..52432d145 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 @@ -1,10 +1,10 @@ -import { KtLogger } from "lib/logger/logger"; +import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; import p from "prudence"; import { FormatPrError } from "utils/prudence"; -import { EmptyObject } from "utils/types"; -import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; -import { ParserFunctionReturns } from "../../common/types"; -import { MerScore } from "./types"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { MerScore } from "./types"; +import type { KtLogger } from "lib/logger/logger"; +import type { EmptyObject } from "utils/types"; const PR_MerIIDX = { music_id: p.isPositiveInteger, @@ -55,7 +55,7 @@ export function ParseMerIIDX( return { classHandler: null, context: {}, - iterable: jsonData as MerScore[], + iterable: jsonData as Array, game: "iidx", }; } diff --git a/server/src/lib/score-import/import-types/file/mer-iidx/types.ts b/server/src/lib/score-import/import-types/file/mer-iidx/types.ts index 91a959e39..ff48aa930 100644 --- a/server/src/lib/score-import/import-types/file/mer-iidx/types.ts +++ b/server/src/lib/score-import/import-types/file/mer-iidx/types.ts @@ -1,19 +1,19 @@ -import { integer } from "tachi-common"; +import type { integer } from "tachi-common"; export interface MerScore { music_id: integer; - play_type: "SINGLE" | "DOUBLE"; - diff_type: "BEGINNER" | "NORMAL" | "HYPER" | "ANOTHER" | "LEGGENDARIA"; + play_type: "DOUBLE" | "SINGLE"; + diff_type: "ANOTHER" | "BEGINNER" | "HYPER" | "LEGGENDARIA" | "NORMAL"; score: integer; miss_count: integer; clear_type: - | "NO PLAY" - | "FAILED" | "ASSIST CLEAR" - | "EASY CLEAR" | "CLEAR" - | "HARD CLEAR" + | "EASY CLEAR" | "EX HARD CLEAR" - | "FULLCOMBO CLEAR"; + | "FAILED" + | "FULLCOMBO CLEAR" + | "HARD CLEAR" + | "NO PLAY"; update_time: string; } diff --git a/server/src/lib/score-import/import-types/file/pli-iidx-csv/parser.ts b/server/src/lib/score-import/import-types/file/pli-iidx-csv/parser.ts index 43259ad68..bac1969fe 100644 --- a/server/src/lib/score-import/import-types/file/pli-iidx-csv/parser.ts +++ b/server/src/lib/score-import/import-types/file/pli-iidx-csv/parser.ts @@ -1,10 +1,10 @@ -import { KtLogger } from "lib/logger/logger"; import GenericParseEamIIDXCSV from "../../common/eamusement-iidx-csv/parser"; -import { +import type { IIDXEamusementCSVContext, IIDXEamusementCSVData, } from "../../common/eamusement-iidx-csv/types"; -import { ParserFunctionReturns } from "../../common/types"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { KtLogger } from "lib/logger/logger"; function ParsePLIIIDXCSV( fileData: Express.Multer.File, 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 b1c5424f6..62d796ac6 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 @@ -1,8 +1,3 @@ -import { KtLogger } from "lib/logger/logger"; -import { Difficulties, Grades, Lamps, Playtypes } from "tachi-common"; -import { FindChartWithPTDFVersion } from "utils/queries/charts"; -import { FindSongOnTitleInsensitive } from "utils/queries/songs"; -import { EmptyObject } from "utils/types"; import { InvalidScoreFailure, KTDataNotFoundFailure, @@ -12,13 +7,18 @@ import { GenericGetGradeAndPercent, ParseDateFromString, } from "../../../framework/common/score-utils"; -import { DryScore } from "../../../framework/common/types"; -import { ConverterFunction } from "../../common/types"; -import { S3Score } from "./types"; +import { FindChartWithPTDFVersion } from "utils/queries/charts"; +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 { EmptyObject } from "utils/types"; export function ParseDifficulty(diff: S3Score["diff"]): { playtype: Playtypes["iidx"]; - difficulty: Difficulties["iidx:SP" | "iidx:DP"]; + difficulty: Difficulties["iidx:DP" | "iidx:SP"]; } { switch (diff) { case "L7": @@ -44,7 +44,7 @@ export function ParseDifficulty(diff: S3Score["diff"]): { } } -export function ResolveS3Lamp(data: S3Score, logger: KtLogger): Lamps["iidx:SP" | "iidx:DP"] { +export function ResolveS3Lamp(data: S3Score, logger: KtLogger): Lamps["iidx:DP" | "iidx:SP"] { if (data.cleartype === "played") { return "FAILED"; } else if (data.cleartype === "cleared") { @@ -54,12 +54,12 @@ export function ResolveS3Lamp(data: S3Score, logger: KtLogger): Lamps["iidx:SP" return "HARD CLEAR"; } else if (data.mods.hardeasy === "E") { return "EASY CLEAR"; - } else { - 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}?` - ); } + + 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" || @@ -143,6 +143,7 @@ export const ConvertFileS3: ConverterFunction = async ( const timeAchieved = ParseDateFromString(data.date); let judgements = {}; + if (data.scorebreakdown) { judgements = { pgreat: data.scorebreakdown.justgreats, @@ -153,14 +154,14 @@ export const ConvertFileS3: ConverterFunction = async ( }; } - const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { + const dryScore: DryScore<"iidx:DP" | "iidx:SP"> = { game: "iidx", comment: data.comment ?? null, importType: "file/solid-state-squad", service: "Solid State Squad", scoreData: { percent, - grade: grade as Grades["iidx:SP" | "iidx:DP"], + grade: grade as Grades["iidx:DP" | "iidx:SP"], score: data.exscore, lamp, judgements, 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 236a7f580..4fb323eb7 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 @@ -1,11 +1,12 @@ -import { XMLParser } from "fast-xml-parser"; -import { KtLogger } from "lib/logger/logger"; -import p, { PrudenceSchema } from "prudence"; -import { FormatPrError } from "utils/prudence"; -import { EmptyObject } from "utils/types"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; -import { ParserFunctionReturns } from "../../common/types"; -import { S3Score } from "./types"; +import { XMLParser } from "fast-xml-parser"; +import p from "prudence"; +import { FormatPrError } from "utils/prudence"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { S3Score } from "./types"; +import type { KtLogger } from "lib/logger/logger"; +import type { PrudenceSchema } from "prudence"; +import type { EmptyObject } from "utils/types"; const PR_SolidState: PrudenceSchema = { s3data: { @@ -86,7 +87,7 @@ export function ParseSolidStateXML( return { classHandler: null, context: {}, - iterable: parsedXML.s3data.scoredata.song as S3Score[], + iterable: parsedXML.s3data.scoredata.song as Array, game: "iidx", }; } diff --git a/server/src/lib/score-import/import-types/file/solid-state-squad/types.ts b/server/src/lib/score-import/import-types/file/solid-state-squad/types.ts index c51575a36..754f980d4 100644 --- a/server/src/lib/score-import/import-types/file/solid-state-squad/types.ts +++ b/server/src/lib/score-import/import-types/file/solid-state-squad/types.ts @@ -1,8 +1,8 @@ -import { integer } from "tachi-common"; +import type { integer } from "tachi-common"; export interface S3Score { id: integer; - diff: 5 | "L7" | 7 | "A" | "B" | "L14" | 14 | "A14" | "B14"; + diff: "A" | "A14" | "B" | "B14" | "L7" | "L14" | 5 | 7 | 14; songname: string; exscore: integer; styles: string; @@ -14,9 +14,9 @@ export interface S3Score { poor: integer; }; mods: { - hardeasy?: "H" | "E"; + hardeasy?: "E" | "H"; }; - cleartype: "cleared" | "played" | "combo" | "comboed" | "perfect" | "perfected"; + cleartype: "cleared" | "combo" | "comboed" | "perfect" | "perfected" | "played"; date: string; comment?: string; } 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 37310b466..4fe532ce7 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 @@ -1,14 +1,14 @@ -import { Lamps } from "tachi-common"; -import { FindSDVXChartOnInGameID } from "utils/queries/charts"; -import { FindSongOnID } from "utils/queries/songs"; import { InternalFailure, KTDataNotFoundFailure, } from "../../../framework/common/converter-failures"; import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; -import { DryScore } from "../../../framework/common/types"; -import { ConverterFunction } from "../../common/types"; -import { BarbatosContext, BarbatosScore } from "./types"; +import { FindSDVXChartOnInGameID } from "utils/queries/charts"; +import { FindSongOnID } from "utils/queries/songs"; +import type { DryScore } from "../../../framework/common/types"; +import type { ConverterFunction } from "../../common/types"; +import type { BarbatosContext, BarbatosScore } from "./types"; +import type { Lamps } from "tachi-common"; const LAMP_LOOKUP = { 1: "FAILED", @@ -33,11 +33,11 @@ export const ConverterIRBarbatos: ConverterFunction { const difficulty = DIFFICULTY_LOOKUP[data.difficulty] as - | "NOV" | "ADV" - | "EXH" | "ANY_INF" - | "MXM"; + | "EXH" + | "MXM" + | "NOV"; const chart = await FindSDVXChartOnInGameID(data.song_id, difficulty); 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 118731a16..c97313b40 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 @@ -1,9 +1,9 @@ -import { KtLogger } from "lib/logger/logger"; +import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; import p from "prudence"; import { FormatPrError } from "utils/prudence"; -import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; -import { ParserFunctionReturns } from "../../common/types"; -import { BarbatosContext, BarbatosScore } from "./types"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { BarbatosContext, BarbatosScore } from "./types"; +import type { KtLogger } from "lib/logger/logger"; const PR_Barbatos = { difficulty: p.isIn(1, 2, 3, 4), @@ -37,7 +37,7 @@ export function ParseBarbatosSingle( return { context: { timeReceived: Date.now() }, game: "sdvx", - iterable: [body] as unknown as BarbatosScore[], + iterable: [body] as unknown as Array, classHandler: null, }; } diff --git a/server/src/lib/score-import/import-types/ir/barbatos/types.ts b/server/src/lib/score-import/import-types/ir/barbatos/types.ts index 9a3ad9956..487d42d71 100644 --- a/server/src/lib/score-import/import-types/ir/barbatos/types.ts +++ b/server/src/lib/score-import/import-types/ir/barbatos/types.ts @@ -1,4 +1,4 @@ -import { integer } from "tachi-common"; +import type { integer } from "tachi-common"; export interface BarbatosScore { difficulty: 0 | 1 | 2 | 3 | 4; 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 980611978..3c51cad43 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 @@ -1,22 +1,23 @@ -import db from "external/mongo/db"; -import { KtLogger } from "lib/logger/logger"; -import { HandleOrphanQueue } from "lib/orphan-queue/orphan-queue"; -import { ReprocessOrphan } from "lib/score-import/framework/orphans/orphans"; -import { ServerConfig, TachiConfig } from "lib/setup/config"; -import { ChartDocument, SongDocument, Playtypes } from "tachi-common"; -import { Random20Hex } from "utils/misc"; -import { GetBlacklist } from "utils/queries/blacklist"; -import { FindChartOnSHA256, FindChartOnSHA256Playtype } from "utils/queries/charts"; -import { FindSongOnID } from "utils/queries/songs"; import { InternalFailure, InvalidScoreFailure, KTDataNotFoundFailure, } from "../../../framework/common/converter-failures"; import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; -import { DryScore } from "../../../framework/common/types"; -import { ConverterFunction } from "../../common/types"; -import { BeatorajaChart, BeatorajaContext, BeatorajaScore } from "./types"; +import db from "external/mongo/db"; +import { HandleOrphanQueue } from "lib/orphan-queue/orphan-queue"; +import { ReprocessOrphan } from "lib/score-import/framework/orphans/orphans"; +import { ServerConfig, TachiConfig } from "lib/setup/config"; +import { Random20Hex } from "utils/misc"; +import { GetBlacklist } from "utils/queries/blacklist"; +import { FindChartOnSHA256, FindChartOnSHA256Playtype } from "utils/queries/charts"; +import { FindSongOnID } from "utils/queries/songs"; +import type { DryScore } from "../../../framework/common/types"; +import type { ConverterFunction } from "../../common/types"; +import type { BeatorajaChart, BeatorajaContext, BeatorajaScore } from "./types"; +import type { KtLogger } from "lib/logger/logger"; +import type { ChartDocument, SongDocument, Playtypes } from "tachi-common"; + const LAMP_LOOKUP = { NoPlay: "NO PLAY", Failed: "FAILED", @@ -145,7 +146,7 @@ export const ConverterIRBeatoraja: ConverterFunction | null; + let chart: ChartDocument<"bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard"> | null; if (game === "bms") { chart = (await FindChartOnSHA256(game, data.sha256)) as ChartDocument< @@ -219,7 +220,7 @@ export const ConverterIRBeatoraja: ConverterFunction = { + const dryScore: DryScore<"bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard"> = { comment: null, game, importType, @@ -244,7 +245,7 @@ export const ConverterIRBeatoraja: ConverterFunction = { + const chartDoc: ChartDocument<"bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard"> = { chartID: Random20Hex(), difficulty: "CHART", isPrimary: true, diff --git a/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts b/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts index cfba96d62..f0699fafb 100644 --- a/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts +++ b/server/src/lib/score-import/import-types/ir/beatoraja/parser.ts @@ -1,10 +1,10 @@ -import { KtLogger } from "lib/logger/logger"; -import p from "prudence"; -import { integer } from "tachi-common"; -import { FormatPrError } from "utils/prudence"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; -import { ParserFunctionReturns } from "../../common/types"; -import { BeatorajaChart, BeatorajaContext, BeatorajaScore } from "./types"; +import p from "prudence"; +import { FormatPrError } from "utils/prudence"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { BeatorajaChart, BeatorajaContext, BeatorajaScore } from "./types"; +import type { KtLogger } from "lib/logger/logger"; +import type { integer } from "tachi-common"; const PR_BeatorajaScore = { sha256: "string", @@ -117,6 +117,7 @@ export function ParseBeatorajaSingle( return { context: { client: isPMS ? "beatoraja" : "lr2oraja", + // asserted using prudence. chart, userID, 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 584fc4e11..5b5cba81d 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 @@ -1,7 +1,7 @@ -import { integer } from "tachi-common"; +import type { integer } from "tachi-common"; export interface BeatorajaContext { - client: "lr2oraja" | "beatoraja"; + client: "beatoraja" | "lr2oraja"; chart: BeatorajaChart; userID: integer; // unexpectedly necessary for orphan code! timeReceived: number; @@ -36,16 +36,16 @@ export interface BeatorajaScore { lntype: 0 | 1; clear: - | "NoPlay" - | "Failed" - | "LightAssistEasy" | "Easy" - | "Normal" - | "Hard" | "ExHard" + | "Failed" | "FullCombo" - | "Perfect" - | "Max"; + | "Hard" + | "LightAssistEasy" + | "Max" + | "NoPlay" + | "Normal" + | "Perfect"; assist: 0; maxcombo: integer; diff --git a/server/src/lib/score-import/import-types/ir/direct-manual/parser.ts b/server/src/lib/score-import/import-types/ir/direct-manual/parser.ts index d304e53b0..662fad70b 100644 --- a/server/src/lib/score-import/import-types/ir/direct-manual/parser.ts +++ b/server/src/lib/score-import/import-types/ir/direct-manual/parser.ts @@ -1,8 +1,8 @@ -import { KtLogger } from "lib/logger/logger"; -import { BatchManualScore } from "tachi-common"; import { ParseBatchManualFromObject } from "../../common/batch-manual/parser"; -import { BatchManualContext } from "../../common/batch-manual/types"; -import { ParserFunctionReturns } from "../../common/types"; +import type { BatchManualContext } from "../../common/batch-manual/types"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { KtLogger } from "lib/logger/logger"; +import type { BatchManualScore } from "tachi-common"; /** * Parses an object of BATCH-MANUAL data. diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.ts index 40183a854..632c6f5bb 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/class-handler.ts @@ -1,5 +1,5 @@ -import { integer } from "tachi-common"; -import { ClassHandler } from "../../../framework/user-game-stats/types"; +import type { ClassHandler } from "../../../framework/user-game-stats/types"; +import type { integer } from "tachi-common"; export function CreateFerStaticClassHandler(body: Record): ClassHandler { return (game, playtype, userID, ratings, logger) => { diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/converter.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/converter.ts index d9435c3d2..758f40fe6 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/converter.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/converter.ts @@ -1,15 +1,15 @@ -import { Lamps } from "tachi-common"; -import { FindChartOnInGameIDVersion } from "utils/queries/charts"; -import { FindSongOnID } from "utils/queries/songs"; import { InternalFailure, KTDataNotFoundFailure, } from "../../../framework/common/converter-failures"; import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; -import { DryScore } from "../../../framework/common/types"; -import { ConverterFunction } from "../../common/types"; import { FERVIDEX_LAMP_LOOKUP, SplitFervidexChartRef } from "../fervidex/converter"; -import { FervidexStaticContext, FervidexStaticScore } from "./types"; +import { FindChartOnInGameIDVersion } from "utils/queries/charts"; +import { FindSongOnID } from "utils/queries/songs"; +import type { DryScore } from "../../../framework/common/types"; +import type { ConverterFunction } from "../../common/types"; +import type { FervidexStaticContext, FervidexStaticScore } from "./types"; +import type { Lamps } from "tachi-common"; export const ConverterIRFervidexStatic: ConverterFunction< FervidexStaticScore, @@ -49,7 +49,7 @@ export const ConverterIRFervidexStatic: ConverterFunction< hitMeta.bp = data.miss_count === -1 ? null : data.miss_count; } - const dryScore: DryScore<"iidx:SP" | "iidx:DP"> = { + const dryScore: DryScore<"iidx:DP" | "iidx:SP"> = { game: "iidx", service: "Fervidex Static", comment: null, @@ -59,7 +59,7 @@ export const ConverterIRFervidexStatic: ConverterFunction< score: data.ex_score, percent, grade, - lamp: FERVIDEX_LAMP_LOOKUP[data.clear_type] as Lamps["iidx:SP" | "iidx:DP"], + lamp: FERVIDEX_LAMP_LOOKUP[data.clear_type] as Lamps["iidx:DP" | "iidx:SP"], judgements: {}, hitMeta, }, 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 4d2d3cbba..2f3414cff 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 @@ -1,12 +1,14 @@ -import { KtLogger } from "lib/logger/logger"; -import p, { PrudenceSchema } from "prudence"; -import { FormatPrError } from "utils/prudence"; +import { CreateFerStaticClassHandler } from "./class-handler"; import { AssertStrAsPositiveInt } from "../../../framework/common/string-asserts"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; -import { ParserFunctionReturns } from "../../common/types"; -import { FerHeaders, SoftwareIDToVersion } from "../fervidex/parser"; -import { CreateFerStaticClassHandler } from "./class-handler"; -import { FervidexStaticContext, FervidexStaticScore } from "./types"; +import { SoftwareIDToVersion } from "../fervidex/parser"; +import p from "prudence"; +import { FormatPrError } from "utils/prudence"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { FerHeaders } from "../fervidex/parser"; +import type { FervidexStaticContext, FervidexStaticScore } from "./types"; +import type { KtLogger } from "lib/logger/logger"; +import type { PrudenceSchema } from "prudence"; const PR_FervidexStatic: PrudenceSchema = { ex_score: p.isPositiveInteger, @@ -21,13 +23,13 @@ export function ParseFervidexStatic( ): ParserFunctionReturns { const version = SoftwareIDToVersion(headers.model, logger); - const staticScores = body?.scores; + const staticScores = body.scores; if (!staticScores || typeof staticScores !== "object") { throw new ScoreImportFatalError(400, `Invalid body.scores.`); } - const scores: FervidexStaticScore[] = []; + const scores: Array = []; for (const songID in staticScores) { // @ts-expect-error pls. @@ -64,6 +66,7 @@ export function ParseFervidexStatic( scores.push({ song_id, + // is asserted with the above check chart: chart as FervidexStaticScore["chart"], clear_type: score.clear_type, diff --git a/server/src/lib/score-import/import-types/ir/fervidex-static/types.ts b/server/src/lib/score-import/import-types/ir/fervidex-static/types.ts index 39988d369..c4c53bdd7 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex-static/types.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex-static/types.ts @@ -1,13 +1,13 @@ -import { GPTSupportedVersions, integer } from "tachi-common"; +import type { GPTSupportedVersions, integer } from "tachi-common"; export interface FervidexStaticScore { clear_type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; ex_score: integer; miss_count: integer | null; // bp, not cbrk - chart: `${"sp" | "dp"}${"b" | "n" | "h" | "a" | "l"}`; + chart: `${"dp" | "sp"}${"a" | "b" | "h" | "l" | "n"}`; song_id: integer; } export interface FervidexStaticContext { - version: GPTSupportedVersions["iidx:SP" | "iidx:DP"]; + version: GPTSupportedVersions["iidx:DP" | "iidx:SP"]; } diff --git a/server/src/lib/score-import/import-types/ir/fervidex/converter.ts b/server/src/lib/score-import/import-types/ir/fervidex/converter.ts index 2a82affb8..97f9481aa 100644 --- a/server/src/lib/score-import/import-types/ir/fervidex/converter.ts +++ b/server/src/lib/score-import/import-types/ir/fervidex/converter.ts @@ -1,15 +1,15 @@ -import { Difficulties, Lamps, Playtypes } from "tachi-common"; -import { FindIIDXChartOnInGameIDVersion, FindIIDXChartWith2DXtraHash } from "utils/queries/charts"; -import { FindSongOnID } from "utils/queries/songs"; import { InternalFailure, InvalidScoreFailure, KTDataNotFoundFailure, } from "../../../framework/common/converter-failures"; import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; -import { DryScore } from "../../../framework/common/types"; -import { ConverterFunction } from "../../common/types"; -import { FervidexContext, FervidexScore } from "./types"; +import { FindIIDXChartOnInGameIDVersion, FindIIDXChartWith2DXtraHash } from "utils/queries/charts"; +import { FindSongOnID } from "utils/queries/songs"; +import type { DryScore } from "../../../framework/common/types"; +import type { ConverterFunction } from "../../common/types"; +import type { FervidexContext, FervidexScore } from "./types"; +import type { Difficulties, Lamps, Playtypes } from "tachi-common"; export const FERVIDEX_LAMP_LOOKUP = { 0: "NO PLAY", @@ -24,7 +24,7 @@ export const FERVIDEX_LAMP_LOOKUP = { export function TachifyAssist( assist: Required["option"]["assist"] -): DryScore<"iidx:SP" | "iidx:DP">["scoreMeta"]["assist"] { +): DryScore<"iidx:DP" | "iidx:SP">["scoreMeta"]["assist"] { switch (assist) { case "FULL_ASSIST": case "ASCR_LEGACY": @@ -41,7 +41,7 @@ export function TachifyAssist( export function TachifyGauge( gauge: Required["option"]["gauge"] -): DryScore<"iidx:SP" | "iidx:DP">["scoreMeta"]["gauge"] { +): DryScore<"iidx:DP" | "iidx:SP">["scoreMeta"]["gauge"] { switch (gauge) { case "ASSISTED_EASY": return "ASSISTED EASY"; @@ -59,7 +59,7 @@ export function TachifyGauge( export function TachifyRange( gauge: Required["option"]["range"] -): DryScore<"iidx:SP" | "iidx:DP">["scoreMeta"]["range"] { +): DryScore<"iidx:DP" | "iidx:SP">["scoreMeta"]["range"] { switch (gauge) { case "HIDDEN_PLUS": return "HIDDEN+"; @@ -95,30 +95,41 @@ export function TachifyRandom(gauge: Required["option"]["style"]) export function SplitFervidexChartRef(ferDif: FervidexScore["chart"]) { let playtype: Playtypes["iidx"]; + if (ferDif.startsWith("sp")) { playtype = "SP"; } else { playtype = "DP"; } - let difficulty: Difficulties["iidx:SP" | "iidx:DP"]; + let difficulty: Difficulties["iidx:DP" | "iidx:SP"]; switch (ferDif[ferDif.length - 1]) { - case "b": + case "b": { difficulty = "BEGINNER"; break; - case "n": + } + + case "n": { difficulty = "NORMAL"; break; - case "h": + } + + case "h": { difficulty = "HYPER"; break; - case "a": + } + + case "a": { difficulty = "ANOTHER"; break; - case "l": + } + + case "l": { difficulty = "LEGGENDARIA"; break; + } + default: throw new InternalFailure(`Invalid fervidex difficulty of ${ferDif}`); } @@ -135,10 +146,12 @@ export const ConverterIRFervidex: ConverterFunction = { + const dryScore: DryScore<"iidx:DP" | "iidx:SP"> = { game: "iidx", service: "Fervidex", comment: null, @@ -191,7 +204,7 @@ export const ConverterIRFervidex: ConverterFunction, classHandler: null, }; } 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 cfef3df77..ce13f6d81 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 @@ -1,10 +1,11 @@ -import { GPTSupportedVersions, integer } from "tachi-common"; +import type { GPTSupportedVersions, integer } from "tachi-common"; export interface FervidexScore { - chart: `${"sp" | "dp"}${"b" | "n" | "h" | "a" | "l"}`; + chart: `${"dp" | "sp"}${"a" | "b" | "h" | "l" | "n"}`; entry_id: integer; chart_sha256?: string; + // whether or whether not this chart is from 2dxtra custom?: boolean; @@ -22,8 +23,8 @@ export interface FervidexScore { ex_score: integer; clear_type: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; - gauge: integer[]; - ghost: integer[]; + gauge: Array; + ghost: Array; dead?: { measure?: integer | null; @@ -31,17 +32,17 @@ export interface FervidexScore { }; option?: { - gauge?: "ASSISTED_EASY" | "EASY" | "HARD" | "EX_HARD" | null; + gauge?: "ASSISTED_EASY" | "EASY" | "EX_HARD" | "HARD" | null; range?: - | "SUDDEN_PLUS" | "HIDDEN_PLUS" - | "SUD_PLUS_HID_PLUS" - | "LIFT" | "LIFT_SUD_PLUS" + | "LIFT" + | "SUD_PLUS_HID_PLUS" + | "SUDDEN_PLUS" | null; - style?: "RANDOM" | "R_RANDOM" | "S_RANDOM" | "MIRROR" | null; - style_2p?: "RANDOM" | "R_RANDOM" | "S_RANDOM" | "MIRROR" | null; - assist?: "AUTO_SCRATCH" | "LEGACY_NOTE" | "ASCR_LEGACY" | "FULL_ASSIST" | null; + style?: "MIRROR" | "R_RANDOM" | "RANDOM" | "S_RANDOM" | null; + style_2p?: "MIRROR" | "R_RANDOM" | "RANDOM" | "S_RANDOM" | null; + assist?: "ASCR_LEGACY" | "AUTO_SCRATCH" | "FULL_ASSIST" | "LEGACY_NOTE" | null; }; pacemaker: { @@ -51,14 +52,14 @@ export interface FervidexScore { }; "2dx-gsm"?: { - EASY: (number | null)[]; - NORMAL: (number | null)[]; - HARD: (number | null)[]; - EX_HARD: (number | null)[]; + EASY: Array; + NORMAL: Array; + HARD: Array; + EX_HARD: Array; } | null; } export interface FervidexContext { - version: GPTSupportedVersions["iidx:SP" | "iidx:DP"]; + version: GPTSupportedVersions["iidx:DP" | "iidx:SP"]; timeReceived: number; } diff --git a/server/src/lib/score-import/import-types/ir/kshook-sv6c/converter.ts b/server/src/lib/score-import/import-types/ir/kshook-sv6c/converter.ts index 57d3048fd..d004b00d5 100644 --- a/server/src/lib/score-import/import-types/ir/kshook-sv6c/converter.ts +++ b/server/src/lib/score-import/import-types/ir/kshook-sv6c/converter.ts @@ -3,12 +3,12 @@ import { KTDataNotFoundFailure, } from "lib/score-import/framework/common/converter-failures"; import { GenericGetGradeAndPercent } from "lib/score-import/framework/common/score-utils"; -import { DryScore } from "lib/score-import/framework/common/types"; -import { Lamps } from "tachi-common"; import { FindSDVXChartOnInGameIDVersion } from "utils/queries/charts"; import { FindSongOnID } from "utils/queries/songs"; -import { ConverterFunction } from "../../common/types"; -import { KsHookSV6CContext, KsHookSV6CScore } from "./types"; +import type { ConverterFunction } from "../../common/types"; +import type { KsHookSV6CContext, KsHookSV6CScore } from "./types"; +import type { DryScore } from "lib/score-import/framework/common/types"; +import type { Lamps } from "tachi-common"; export const ConverterIRKsHookSV6C: ConverterFunction = async ( data, @@ -81,7 +81,7 @@ function ConvertLamp(clear: KsHookSV6CScore["clear"]): Lamps["sdvx:Single"] { function ConvertDifficulty( diff: KsHookSV6CScore["difficulty"] -): "NOV" | "ADV" | "EXH" | "ANY_INF" | "MXM" { +): "ADV" | "ANY_INF" | "EXH" | "MXM" | "NOV" { if (diff === "DIFFICULTY_NOVICE") { return "NOV"; } else if (diff === "DIFFICULTY_ADVANCED") { 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 7ee4ebd6d..fead42636 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 @@ -1,9 +1,10 @@ -import { KtLogger } from "lib/logger/logger"; import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error"; -import p, { PrudenceSchema } from "prudence"; +import p from "prudence"; import { FormatPrError } from "utils/prudence"; -import { ParserFunctionReturns } from "../../common/types"; -import { KsHookSV6CContext, KsHookSV6CScore } from "./types"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { KsHookSV6CContext, KsHookSV6CScore } from "./types"; +import type { KtLogger } from "lib/logger/logger"; +import type { PrudenceSchema } from "prudence"; const PR_KsHookSV6C: PrudenceSchema = { clear: p.isIn( diff --git a/server/src/lib/score-import/import-types/ir/kshook-sv6c/types.ts b/server/src/lib/score-import/import-types/ir/kshook-sv6c/types.ts index 5e7ed5416..3e9f45bca 100644 --- a/server/src/lib/score-import/import-types/ir/kshook-sv6c/types.ts +++ b/server/src/lib/score-import/import-types/ir/kshook-sv6c/types.ts @@ -1,24 +1,24 @@ -import { integer } from "tachi-common"; +import type { integer } from "tachi-common"; export interface KsHookSV6CScore { clear: - | "CLEAR_PLAYED" | "CLEAR_EFFECTIVE" | "CLEAR_EXCESSIVE" - | "CLEAR_ULTIMATE_CHAIN" - | "CLEAR_PERFECT"; - difficulty: `DIFFICULTY_${"NOVICE" | "ADVANCED" | "EXHAUST" | "INFINITE" | "MAXIMUM"}`; + | "CLEAR_PERFECT" + | "CLEAR_PLAYED" + | "CLEAR_ULTIMATE_CHAIN"; + difficulty: `DIFFICULTY_${"ADVANCED" | "EXHAUST" | "INFINITE" | "MAXIMUM" | "NOVICE"}`; gauge: integer; grade: `GRADE_${ - | "D" - | "C" - | "B" - | "A" | "A_PLUS" - | "AA" + | "A" | "AA_PLUS" - | "AAA" + | "AA" | "AAA_PLUS" + | "AAA" + | "B" + | "C" + | "D" | "S"}`; max_chain: integer; music_id: integer; @@ -27,7 +27,7 @@ export interface KsHookSV6CScore { near: integer; error: integer; - rate: `RATE_${"EFFECTIVE" | "PERMISSIVE" | "EXCESSIVE" | "BLASTIVE"}`; + rate: `RATE_${"BLASTIVE" | "EFFECTIVE" | "EXCESSIVE" | "PERMISSIVE"}`; score: integer; ex_score: integer; diff --git a/server/src/lib/score-import/import-types/ir/lr2hook/converter.ts b/server/src/lib/score-import/import-types/ir/lr2hook/converter.ts index 5d3a65fea..0aefb0748 100644 --- a/server/src/lib/score-import/import-types/ir/lr2hook/converter.ts +++ b/server/src/lib/score-import/import-types/ir/lr2hook/converter.ts @@ -3,12 +3,12 @@ import { KTDataNotFoundFailure, } from "lib/score-import/framework/common/converter-failures"; import { GenericGetGradeAndPercent } from "lib/score-import/framework/common/score-utils"; -import { DryScore } from "lib/score-import/framework/common/types"; -import { Lamps, ScoreDocument } from "tachi-common"; import { FindBMSChartOnHash } from "utils/queries/charts"; import { FindSongOnID } from "utils/queries/songs"; -import { ConverterFunction } from "../../common/types"; -import { LR2HookContext, LR2HookScore } from "./types"; +import type { ConverterFunction } from "../../common/types"; +import type { LR2HookContext, LR2HookScore } from "./types"; +import type { DryScore } from "lib/score-import/framework/common/types"; +import type { Lamps, ScoreDocument } from "tachi-common"; export const ConverterLR2Hook: ConverterFunction = async ( data, 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 0e97a76f9..f5e718029 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 @@ -1,9 +1,10 @@ -import { KtLogger } from "lib/logger/logger"; -import { ParserFunctionReturns } from "../../common/types"; -import { LR2HookContext, LR2HookScore } from "./types"; -import p, { PrudenceSchema } from "prudence"; import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error"; +import p from "prudence"; import { FormatPrError } from "utils/prudence"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { LR2HookContext, LR2HookScore } from "./types"; +import type { KtLogger } from "lib/logger/logger"; +import type { PrudenceSchema } from "prudence"; const SUPPORTED_RANDOMS = ["NORAN", "MIRROR", "RAN", "S-RAN"]; @@ -11,9 +12,11 @@ export const PR_LR2Hook: PrudenceSchema = { md5: "string", playerData: { autoScr: p.is(0), + // don't really need this, but it's // { "ALL", "SINGLE", "7K", "5K", "DOUBLE", "14K", "10K", "9K" }; gameMode: p.any, + // ALLSCR and H-RAN may also be sent, but we don't support them. random: p.isIn(SUPPORTED_RANDOMS), gauge: p.isIn("GROOVE", "HARD", "HAZARD", "EASY", "P-ATTACK", "G-ATTACK"), @@ -34,9 +37,11 @@ export const PR_LR2Hook: PrudenceSchema = { if (!Array.isArray(self)) { return "Expected an array"; } + if (self.length !== 1000) { return "Expected an array with length 1000."; } + if (self.some((x) => x < 0 || x > 100 || !Number.isInteger(x))) { return "Expected an array of 1000 integers between 0 and 100."; } diff --git a/server/src/lib/score-import/import-types/ir/lr2hook/types.ts b/server/src/lib/score-import/import-types/ir/lr2hook/types.ts index 4be600234..7e4248ca5 100644 --- a/server/src/lib/score-import/import-types/ir/lr2hook/types.ts +++ b/server/src/lib/score-import/import-types/ir/lr2hook/types.ts @@ -1,12 +1,12 @@ -import { integer } from "tachi-common"; +import type { integer } from "tachi-common"; export interface LR2HookScore { md5: string; playerData: { autoScr: 0; gameMode: unknown; - random: "NORAN" | "MIRROR" | "RAN" | "S-RAN"; - gauge: "GROOVE" | "HAZARD" | "HARD" | "EASY" | "P-ATTACK" | "G-ATTACK"; + random: "MIRROR" | "NORAN" | "RAN" | "S-RAN"; + gauge: "EASY" | "G-ATTACK" | "GROOVE" | "HARD" | "HAZARD" | "P-ATTACK"; }; scoreData: { pgreat: integer; @@ -19,8 +19,8 @@ export interface LR2HookScore { moneyScore: integer; notesTotal: integer; notesPlayed: integer; - lamp: "NO PLAY" | "FAIL" | "EASY" | "NORMAL" | "HARD" | "FULL COMBO"; - hpGraph: integer[]; + lamp: "EASY" | "FAIL" | "FULL COMBO" | "HARD" | "NO PLAY" | "NORMAL"; + hpGraph: Array; }; } 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 27955b397..2e09c2a24 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 @@ -1,3 +1,9 @@ +import { + InternalFailure, + InvalidScoreFailure, + KTDataNotFoundFailure, +} from "../../../framework/common/converter-failures"; +import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; import db from "external/mongo/db"; import { USC_DEFAULT_HOLD, @@ -6,24 +12,18 @@ import { USC_DEFAULT_PERFECT, USC_DEFAULT_SLAM, } from "lib/constants/usc-ir"; -import { KtLogger } from "lib/logger/logger"; -import { USCClientScore } from "server/router/ir/usc/_playtype/types"; -import { Lamps } from "tachi-common"; import { FindSongOnID } from "utils/queries/songs"; -import { - InternalFailure, - InvalidScoreFailure, - KTDataNotFoundFailure, -} from "../../../framework/common/converter-failures"; -import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils"; -import { DryScore } from "../../../framework/common/types"; -import { ConverterFunction } from "../../common/types"; -import { IRUSCContext } from "./types"; +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"; /** * Interprets the "note mod" used based on the USC score. */ -export function DeriveNoteMod(data: USCClientScore): "NORMAL" | "MIRROR" | "RANDOM" | "MIR-RAN" { +export function DeriveNoteMod(data: USCClientScore): "MIR-RAN" | "MIRROR" | "NORMAL" | "RANDOM" { if (data.options.mirror && data.options.random) { return "MIR-RAN"; } else if (data.options.mirror) { @@ -41,7 +41,7 @@ export function DeriveNoteMod(data: USCClientScore): "NORMAL" | "MIRROR" | "RAND export function DeriveLamp( scoreDoc: USCClientScore, logger: KtLogger -): Lamps["usc:Keyboard" | "usc:Controller"] { +): Lamps["usc:Controller" | "usc:Keyboard"] { if (scoreDoc.score === 10_000_000) { return "PERFECT ULTIMATE CHAIN"; } else if (scoreDoc.error === 0) { 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 74df7ad84..371747801 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 @@ -1,11 +1,12 @@ -import { KtLogger } from "lib/logger/logger"; -import p, { PrudenceSchema } from "prudence"; -import { FormatPrError } from "utils/prudence"; import ScoreImportFatalError from "../../../framework/score-importing/score-import-error"; -import { ParserFunctionReturns } from "../../common/types"; -import { IRUSCContext } from "./types"; -import { Playtypes } from "tachi-common"; -import { USCClientScore } from "server/router/ir/usc/_playtype/types"; +import p from "prudence"; +import { FormatPrError } from "utils/prudence"; +import type { ParserFunctionReturns } from "../../common/types"; +import type { IRUSCContext } from "./types"; +import type { KtLogger } from "lib/logger/logger"; +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 = { score: p.isBoundedInteger(0, 10_000_000), @@ -65,7 +66,7 @@ export function ParseIRUSC( timeReceived: Date.now(), }, game: "usc", - iterable: [score] as USCClientScore[], + iterable: [score] as Array, classHandler: null, }; } diff --git a/server/src/lib/score-import/import-types/ir/usc/types.ts b/server/src/lib/score-import/import-types/ir/usc/types.ts index e9283d56c..b95483c74 100644 --- a/server/src/lib/score-import/import-types/ir/usc/types.ts +++ b/server/src/lib/score-import/import-types/ir/usc/types.ts @@ -1,4 +1,4 @@ -import { Playtypes } from "tachi-common"; +import type { Playtypes } from "tachi-common"; export interface IRUSCContext { chartHash: string; diff --git a/server/src/lib/score-import/worker/types.ts b/server/src/lib/score-import/worker/types.ts index 6e1f5d8ea..dcc0c96f8 100644 --- a/server/src/lib/score-import/worker/types.ts +++ b/server/src/lib/score-import/worker/types.ts @@ -1,15 +1,15 @@ -import { Job } from "bullmq"; -import { ImportTypes, integer } from "tachi-common"; -import { Parsers } from "../import-types/parsers"; +import type { Parsers } from "../import-types/parsers"; +import type { Job } from "bullmq"; +import type { 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. // This is because parser functions have variable signatures, but all // end with requiring a logger. We don't want to have to pass the logger, // we just want to pass those other arguments. -type RemoveLast = T extends [...infer RemoveLast, unknown] +type RemoveLast> = T extends [...infer RemoveLast, unknown] ? RemoveLast - : unknown[]; + : Array; // Which means we can use generic access to make ParserArguments<"ir/usc"> // A type that returns the paramaters of the parser function bound to diff --git a/server/src/lib/score-import/worker/worker.ts b/server/src/lib/score-import/worker/worker.ts index 624b119ff..cd60e05c2 100644 --- a/server/src/lib/score-import/worker/worker.ts +++ b/server/src/lib/score-import/worker/worker.ts @@ -1,15 +1,16 @@ -import { Worker } from "bullmq"; -import { EventEmitter } from "events"; -import { HandleSIGTERMGracefully } from "lib/handlers/sigterm"; -import CreateLogCtx from "lib/logger/logger"; -import { Environment, ServerConfig } from "lib/setup/config"; -import { ImportTypes } from "tachi-common"; -import { FormatUserDoc, GetUserWithID } from "utils/user"; +import ScoreImportQueue from "./queue"; import { GetInputParser } from "../framework/common/get-input-parser"; import ScoreImportFatalError from "../framework/score-importing/score-import-error"; import ScoreImportMain from "../framework/score-importing/score-import-main"; -import ScoreImportQueue from "./queue"; -import { ScoreImportJob } from "./types"; +import { Worker } from "bullmq"; +import { HandleSIGTERMGracefully } from "lib/handlers/sigterm"; +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 { ImportTypes } from "tachi-common"; + EventEmitter.defaultMaxListeners = 20; // For scaling performance, running score-importing in a separate worker is preferable @@ -66,7 +67,8 @@ export const worker = new Worker( // eslint-disable-next-line @typescript-eslint/no-explicit-any const processedArgs: any = []; // eslint-disable-next-line @typescript-eslint/no-explicit-any - for (const arg of job.data.parserArguments as any[]) { + + for (const arg of job.data.parserArguments as Array) { if (arg?.buffer?.type === "Buffer") { processedArgs.push({ ...arg, buffer: Buffer.from(arg.buffer.data) }); } else { @@ -138,4 +140,6 @@ worker.on("completed", (job, result) => { logger.debug(`Job ${job.id} finished successfully.`, result); }); -process.on("SIGTERM", () => HandleSIGTERMGracefully()); +process.on("SIGTERM", () => { + HandleSIGTERMGracefully(); +}); diff --git a/server/src/lib/score-mutation/delete-scores.ts b/server/src/lib/score-mutation/delete-scores.ts index c94716a88..cf19eed33 100644 --- a/server/src/lib/score-mutation/delete-scores.ts +++ b/server/src/lib/score-mutation/delete-scores.ts @@ -4,7 +4,7 @@ import CreateLogCtx from "lib/logger/logger"; import { UpdateChartRanking } from "lib/score-import/framework/pb/create-pb-doc"; import { ProcessPBs } from "lib/score-import/framework/pb/process-pbs"; import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/user-game-stats/update-ugs"; -import { Game, Playtype, ScoreDocument } from "tachi-common"; +import type { Game, Playtype, ScoreDocument } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -113,7 +113,7 @@ export async function DeleteScore( } } -export async function DeleteMultipleScores(scores: ScoreDocument[], blacklist = false) { +export async function DeleteMultipleScores(scores: Array, blacklist = false) { logger.info(`received request to delete ${scores.length} (Blacklist: ${blacklist}).`); const scoreIDs = scores.map((e) => e.scoreID); diff --git a/server/src/lib/score-mutation/update-score.ts b/server/src/lib/score-mutation/update-score.ts index ddecb1c07..1ac2c65e9 100644 --- a/server/src/lib/score-mutation/update-score.ts +++ b/server/src/lib/score-mutation/update-score.ts @@ -1,12 +1,13 @@ /* eslint-disable no-await-in-loop */ import db from "external/mongo/db"; -import { KtLogger, rootLogger } from "lib/logger/logger"; +import { rootLogger } from "lib/logger/logger"; import { CreateCalculatedData } from "lib/score-import/framework/calculated-data/calculated-data"; import { UpdateChartRanking } from "lib/score-import/framework/pb/create-pb-doc"; import { CreateScoreID } from "lib/score-import/framework/score-importing/score-id"; -import { ScoreDocument } from "tachi-common"; import { UpdateAllPBs } from "utils/calculations/recalc-scores"; import { FormatUserDoc, GetUserWithID } from "utils/user"; +import type { KtLogger } from "lib/logger/logger"; +import type { ScoreDocument } from "tachi-common"; /** * Updates a score from oldScore to newScore, applying all necessary state @@ -77,6 +78,7 @@ export default async function UpdateScore( logger.warn( `Passed a score with _id to UpdateScore. This property should not be set. Deleting this property and continuing anyway.` ); + // This property shouldn't be defined. delete newScore._id; } @@ -118,10 +120,10 @@ export default async function UpdateScore( const percentDiff = oldScore.scoreData.percent - newScore.scoreData.percent; const scoreDiff = oldScore.scoreData.score - newScore.scoreData.score; - scoreInfo.gradeDelta -= gradeDiff; - scoreInfo.lampDelta -= lampDiff; - scoreInfo.scoreDelta -= scoreDiff; - scoreInfo.percentDelta -= percentDiff; + scoreInfo.gradeDelta = scoreInfo.gradeDelta - gradeDiff; + scoreInfo.lampDelta = scoreInfo.lampDelta - lampDiff; + scoreInfo.scoreDelta = scoreInfo.scoreDelta - scoreDiff; + scoreInfo.percentDelta = scoreInfo.percentDelta - percentDiff; } } } diff --git a/server/src/lib/search/search.ts b/server/src/lib/search/search.ts index 82dc731a3..4ef7e50ea 100644 --- a/server/src/lib/search/search.ts +++ b/server/src/lib/search/search.ts @@ -2,9 +2,11 @@ import deepmerge from "deepmerge"; import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; import { TachiConfig } from "lib/setup/config"; -import { FilterQuery } from "mongodb"; -import { ICollection } from "monk"; -import { +import { EscapeStringRegexp } from "utils/misc"; +import { GetOnlineCutoff } from "utils/user"; +import type { FilterQuery } from "mongodb"; +import type { ICollection } from "monk"; +import type { ChartDocument, Game, integer, @@ -13,8 +15,6 @@ import { SessionDocument, SongDocument, } from "tachi-common"; -import { EscapeStringRegexp } from "utils/misc"; -import { GetOnlineCutoff } from "utils/user"; const logger = CreateLogCtx(__filename); @@ -31,12 +31,13 @@ export function SearchCollection( search: string, existingMatch: FilterQuery = {}, limit = 100 -): Promise<(T & { __textScore: number })[]> { +): Promise> { return collection .aggregate([ { $match: deepmerge({ $text: { $search: search } }, existingMatch), }, + // Project the __textScore field on so we can sort // based on search proximity to query { @@ -44,8 +45,10 @@ export function SearchCollection( __textScore: { $meta: "textScore" }, }, }, + // sort by quality of match { $sort: { __textScore: -1 } }, + // hide nonsense { $match: { __textScore: { $gt: 0.25 } } }, { $limit: limit }, @@ -62,15 +65,15 @@ export function SearchCollection( }); } -export type SongSearchReturn = { +export type SongSearchReturn = SongDocument & { __textScore: number; -} & SongDocument; +}; export function SearchGameSongs( game: Game, search: string, limit = 100 -): Promise { +): Promise> { return SearchCollection(db.songs[game], search, {}, limit); } @@ -90,7 +93,7 @@ export async function SearchGameSongsAndCharts( chartQuery.playtype = playtype; } - const charts = (await db.charts[game].find(chartQuery)) as ChartDocument[]; + const charts = (await db.charts[game].find(chartQuery)) as Array; return { songs, charts }; } @@ -148,9 +151,11 @@ export function SearchUsersRegExp(search: string, matchOnline = false) { * Searches a single game, but optimised for the searchAllGames return. */ async function SearchAllGamesSingleGame(game: Game, search: string) { - const songs = (await SearchGameSongs(game, search, 10)) as (SongSearchReturn & { - game: Game; - })[]; + const songs = (await SearchGameSongs(game, search, 10)) as Array< + SongSearchReturn & { + game: Game; + } + >; for (const song of songs) { song.game = game; diff --git a/server/src/lib/setup/config.ts b/server/src/lib/setup/config.ts index d2de97f81..da0251633 100644 --- a/server/src/lib/setup/config.ts +++ b/server/src/lib/setup/config.ts @@ -1,13 +1,14 @@ /* eslint-disable no-console */ // barrel file for re-exporting env variables. import dotenv from "dotenv"; -import fs from "fs"; import JSON5 from "json5"; -import { SendMailOptions } from "nodemailer"; import p from "prudence"; -import { Game, ImportTypes, integer, StaticConfig } from "tachi-common"; -import { URL } from "url"; +import { StaticConfig } from "tachi-common"; import { FormatPrError } from "utils/prudence"; +import fs from "fs"; +import { URL } from "url"; +import type { SendMailOptions } from "nodemailer"; +import type { Game, ImportTypes, integer } from "tachi-common"; dotenv.config(); // imports things like NODE_ENV from a local .env file if one is present. @@ -70,6 +71,7 @@ export interface TachiServerConfig { EMAIL_CONFIG?: { FROM: string; DKIM?: SendMailOptions["dkim"]; + // @warning This is explicitly allowed to be any // As nodemailer doesnt properly export the types we care about // This should be set to SMTPTransport.Options, but it is @@ -89,24 +91,23 @@ export interface TachiServerConfig { }; TACHI_CONFIG: { NAME: string; - TYPE: "ktchi" | "btchi" | "omni"; - GAMES: Game[]; - IMPORT_TYPES: ImportTypes[]; + TYPE: "btchi" | "ktchi" | "omni"; + GAMES: Array; + IMPORT_TYPES: Array; }; LOGGER_CONFIG: { - LOG_LEVEL: "debug" | "verbose" | "info" | "warn" | "error" | "severe" | "crit"; + LOG_LEVEL: "crit" | "debug" | "error" | "info" | "severe" | "verbose" | "warn"; CONSOLE: boolean; FILE: boolean; SEQ_API_KEY: string | undefined; DISCORD?: { WEBHOOK_URL: string; - WHO_TO_TAG: string[]; + WHO_TO_TAG: Array; }; }; CDN_CONFIG: { WEB_LOCATION: string; SAVE_LOCATION: - | { TYPE: "LOCAL_FILESYSTEM"; LOCATION: string; SERVE_OWN_CDN?: boolean } | { TYPE: "S3_BUCKET"; ENDPOINT: string; @@ -115,7 +116,8 @@ export interface TachiServerConfig { BUCKET: string; KEY_PREFIX?: string; REGION?: string; - }; + } + | { TYPE: "LOCAL_FILESYSTEM"; LOCATION: string; SERVE_OWN_CDN?: boolean }; }; } @@ -147,6 +149,7 @@ const err = p(config, { EMAIL_CONFIG: p.optional({ FROM: "string", DKIM: "*object", + // WARN: This validation is improper and lazy. // The actual content is just some wacky options object. // I'm not going to assert this properly. @@ -217,14 +220,12 @@ tachiServerConfig.MAX_GOAL_SUBSCRIPTIONS ??= 1_000; tachiServerConfig.MAX_MILESTONE_SUBSCRIPTIONS ??= 100; // Assign sane defaults to the logger config. -tachiServerConfig.LOGGER_CONFIG = Object.assign( - { - LOG_LEVEL: "info", - CONSOLE: true, - FILE: true, - }, - tachiServerConfig.LOGGER_CONFIG ?? {} -); +tachiServerConfig.LOGGER_CONFIG = { + LOG_LEVEL: "info", + CONSOLE: true, + FILE: true, + ...(tachiServerConfig.LOGGER_CONFIG ?? {}), +}; export const TachiConfig = tachiServerConfig.TACHI_CONFIG; export const ServerConfig = tachiServerConfig; @@ -232,12 +233,14 @@ export const ServerConfig = tachiServerConfig; // Environment Variable Validation let port = Number(process.env.PORT); + if (Number.isNaN(port) && process.env.IS_SERVER) { logger.warn(`No/invalid PORT specified in environment, defaulting to 8080.`); port = 8080; } const redisUrl = process.env.REDIS_URL; + if (!redisUrl) { // n.b. These logs should be critical level, but the logger cant actually instantiate // itself in this file, because this file also controlls the logger. Ouch! @@ -246,12 +249,14 @@ if (!redisUrl) { } const mongoUrl = process.env.MONGO_URL; + if (!mongoUrl) { logger.error(`No MONGO_URL specified in environment. Terminating.`); process.exit(1); } const seqUrl = process.env.SEQ_URL; + if (!seqUrl && tachiServerConfig.LOGGER_CONFIG.SEQ_API_KEY) { logger.warn( `No SEQ_URL specified in environment, yet LOGGER_CONFIG.SEQ_API_KEY was defined. No logs will be sent to Seq!` @@ -259,6 +264,7 @@ if (!seqUrl && tachiServerConfig.LOGGER_CONFIG.SEQ_API_KEY) { } const nodeEnv = process.env.NODE_ENV; + if (!nodeEnv) { logger.error(`No NODE_ENV specified in environment. Terminating.`); process.exit(1); diff --git a/server/src/lib/showcase/evaluator.ts b/server/src/lib/showcase/evaluator.ts index 27efa3122..b7127da0e 100644 --- a/server/src/lib/showcase/evaluator.ts +++ b/server/src/lib/showcase/evaluator.ts @@ -1,6 +1,11 @@ -import { integer, ShowcaseStatDetails, ShowcaseStatChart, ShowcaseStatFolder } from "tachi-common"; import db from "external/mongo/db"; import { GetFolderChartIDs } from "utils/folder"; +import type { + integer, + ShowcaseStatDetails, + ShowcaseStatChart, + ShowcaseStatFolder, +} from "tachi-common"; export function EvaluateShowcaseStat( details: ShowcaseStatDetails, @@ -43,6 +48,7 @@ async function EvaluateShowcaseChartStat(details: ShowcaseStatChart, userID: int async function EvaluateShowcaseFolderStat(details: ShowcaseStatFolder, userID: integer) { let chartIDs; + if (Array.isArray(details.folderID)) { chartIDs = (await Promise.all(details.folderID.map(GetFolderChartIDs))).flat(1); } else { @@ -53,6 +59,7 @@ async function EvaluateShowcaseFolderStat(details: ShowcaseStatFolder, userID: i const value = await db["personal-bests"].count({ userID, + // @optimisable - This is slightly inefficent, maybe we can use relational-style querying? chartID: { $in: chartIDs }, [mongoProp]: { $gte: details.gte }, @@ -61,7 +68,7 @@ async function EvaluateShowcaseFolderStat(details: ShowcaseStatFolder, userID: i return { value, outOf: chartIDs.length }; } -function PropToMongoProp(prop: "score" | "lamp" | "grade" | "percent") { +function PropToMongoProp(prop: "grade" | "lamp" | "percent" | "score") { switch (prop) { case "score": return "scoreData.score"; @@ -74,7 +81,7 @@ function PropToMongoProp(prop: "score" | "lamp" | "grade" | "percent") { } } -function PropToScoreDataProp(prop: "score" | "lamp" | "grade" | "percent") { +function PropToScoreDataProp(prop: "grade" | "lamp" | "percent" | "score") { switch (prop) { case "score": return "score"; diff --git a/server/src/lib/showcase/get-related.ts b/server/src/lib/showcase/get-related.ts index 368f037b4..a657e18ab 100644 --- a/server/src/lib/showcase/get-related.ts +++ b/server/src/lib/showcase/get-related.ts @@ -1,6 +1,6 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; -import { Game, ShowcaseStatDetails } from "tachi-common"; +import type { Game, ShowcaseStatDetails } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -13,7 +13,7 @@ export async function GetRelatedStatDocuments(stat: ShowcaseStatDetails, game: G throw new Error(`Stat refers to a chart that does not exist? ${stat.chartID}.`); } - const song = await db.songs[game].findOne({ id: chart?.songID }); + const song = await db.songs[game].findOne({ id: chart.songID }); if (!song) { logger.severe(`Song-Chart Mismatch - ${chart.songID}.`, { chart }); diff --git a/server/src/lib/showcase/get-stats.ts b/server/src/lib/showcase/get-stats.ts index a15e5be64..c8369e33d 100644 --- a/server/src/lib/showcase/get-stats.ts +++ b/server/src/lib/showcase/get-stats.ts @@ -1,8 +1,8 @@ -import db from "external/mongo/db"; -import { integer, Game, Playtype, ShowcaseStatDetails } from "tachi-common"; -import CreateLogCtx from "lib/logger/logger"; import { EvaluateShowcaseStat } from "./evaluator"; import { GetRelatedStatDocuments } from "./get-related"; +import db from "external/mongo/db"; +import CreateLogCtx from "lib/logger/logger"; +import type { integer, Game, Playtype, ShowcaseStatDetails } from "tachi-common"; const logger = CreateLogCtx(__filename); diff --git a/server/src/lib/showcase/showcase.ts b/server/src/lib/showcase/showcase.ts index 4cb373b4b..8afb11d98 100644 --- a/server/src/lib/showcase/showcase.ts +++ b/server/src/lib/showcase/showcase.ts @@ -4,7 +4,7 @@ import db from "external/mongo/db"; * When a folder is removed, any showcase stats referring to that folder * need to be changed aswell. */ -export function RemoveStaleFolderShowcaseStats(removedFolderIDs: string[]) { +export function RemoveStaleFolderShowcaseStats(removedFolderIDs: Array) { return db["game-settings"].update( {}, { diff --git a/server/src/lib/targets/goal-utils.ts b/server/src/lib/targets/goal-utils.ts index 2dc750181..30af9bb81 100644 --- a/server/src/lib/targets/goal-utils.ts +++ b/server/src/lib/targets/goal-utils.ts @@ -1,17 +1,10 @@ import db from "external/mongo/db"; import { GenericCalculatePercent } from "lib/score-import/framework/common/score-utils"; -import { - ChartDocument, - FormatGame, - Game, - GetGamePTConfig, - GoalDocument, - Playtype, - Playtypes, -} from "tachi-common"; +import { FormatGame, GetGamePTConfig, Playtypes } from "tachi-common"; import { GetFolderForIDGuaranteed, HumaniseChartID } from "utils/db"; import { GetFolderChartIDs } from "utils/folder"; import { FormatMaxDP, HumanisedJoinArray } from "utils/misc"; +import type { ChartDocument, Game, GoalDocument, Playtype } from "tachi-common"; export async function CreateGoalTitle( charts: GoalDocument["charts"], @@ -35,8 +28,9 @@ export async function CreateGoalTitle( case "folder": return `${formattedCriteria} any chart in ${datasetName}`; } + // eslint-disable-next-line no-fallthrough - case "absolute": + case "absolute": { switch (charts.type) { case "any": return `${formattedCriteria} ${criteria.countNum} charts`; @@ -45,10 +39,12 @@ export async function CreateGoalTitle( case "folder": return `${formattedCriteria} ${criteria.countNum} charts in ${datasetName}`; } + break; - case "proportion": - // I know declaring inside cases is a footgun, but I know better. - // eslint-disable-next-line no-case-declarations + } + + 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) { @@ -59,6 +55,7 @@ export async function CreateGoalTitle( case "folder": return `${formattedCriteria} ${propFormat}% of the charts in ${datasetName}`; } + } } throw new Error(`Couldn't create title from provided info. Is the provided data supported?`); @@ -113,7 +110,9 @@ export async function ValidateGoalChartsAndCriteria( playtype: Playtype ) { let chartCount = 0; + // Validating the charts supplied + if (charts.type === "single") { const chart = await db.charts[game].findOne({ playtype, @@ -191,6 +190,7 @@ export async function ValidateGoalChartsAndCriteria( // checking whether the key and value make sense const gptConfig = GetGamePTConfig(game, playtype); + if (criteria.key === "scoreData.gradeIndex" && !gptConfig.grades[criteria.value]) { throw new Error( `Invalid value of ${criteria.value} for grade goal. No such grade exists at that index.` @@ -223,7 +223,7 @@ export async function ValidateGoalChartsAndCriteria( playtype, chartID: charts.data, }))! as ChartDocument< - "bms:14K" | "bms:7K" | "iidx:SP" | "iidx:DP" | "pms:Controller" | "pms:Keyboard" + "bms:7K" | "bms:14K" | "iidx:DP" | "iidx:SP" | "pms:Controller" | "pms:Keyboard" >; const notecount = relatedChart.data.notecount; diff --git a/server/src/lib/targets/goals.ts b/server/src/lib/targets/goals.ts index 459c4332b..35bb97bb9 100644 --- a/server/src/lib/targets/goals.ts +++ b/server/src/lib/targets/goals.ts @@ -1,11 +1,14 @@ +import { CreateGoalTitle as CreateGoalName, ValidateGoalChartsAndCriteria } from "./goal-utils"; import db from "external/mongo/db"; import fjsh from "fast-json-stable-hash"; import { SubscribeFailReasons } from "lib/constants/err-codes"; -import CreateLogCtx, { KtLogger } from "lib/logger/logger"; -import { FilterQuery } from "mongodb"; -import { +import CreateLogCtx from "lib/logger/logger"; +import { GetGamePTConfig } from "tachi-common"; +import { GetFolderChartIDs } from "utils/folder"; +import type { KtLogger } from "lib/logger/logger"; +import type { FilterQuery } from "mongodb"; +import type { Game, - GetGamePTConfig, GoalDocument, integer, PBScoreDocument, @@ -14,8 +17,6 @@ import { MilestoneSubscriptionDocument, MilestoneDocument, } from "tachi-common"; -import { GetFolderChartIDs } from "utils/folder"; -import { CreateGoalTitle as CreateGoalName, ValidateGoalChartsAndCriteria } from "./goal-utils"; const logger = CreateLogCtx(__filename); @@ -68,6 +69,7 @@ export async function EvaluateGoalForUser( userID, game: goal.game, playtype: goal.playtype, + // normally, this would be a VERY WORRYING line of code, but goal.criteria.key is guaranteed to be // within a specific set of fields. [goal.criteria.key]: { $gte: goal.criteria.value }, @@ -80,12 +82,13 @@ export async function EvaluateGoalForUser( // Next, we need to figure out our criteria. if (goal.criteria.mode === "single") { const res = await db["personal-bests"].findOne(scoreQuery); + // hack, but guaranteed to work. const scoreDataKey = goal.criteria.key.split(".")[1] as - | "lampIndex" | "gradeIndex" - | "score" - | "percent"; + | "lampIndex" + | "percent" + | "score"; const outOfHuman = HumaniseGoalProgress( goal.game, @@ -198,7 +201,9 @@ export async function EvaluateGoalForUser( * * @returns An array of chartIDs, except if the goal chart type is "any", in which case, it returns null. */ -function ResolveGoalCharts(goal: GoalDocument): Promise | string[] | null | undefined { +function ResolveGoalCharts( + goal: GoalDocument +): Array | Promise> | null | undefined { if (goal.charts.type === "single") { return [goal.charts.data]; } else if (goal.charts.type === "multi") { @@ -213,7 +218,7 @@ function ResolveGoalCharts(goal: GoalDocument): Promise | string[] | n type GoalKeys = GoalDocument["criteria"]["key"]; type PBWithBadPoor = PBScoreDocument< - "iidx:SP" | "iidx:DP" | "bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard" + "bms:7K" | "bms:14K" | "iidx:DP" | "iidx:SP" | "pms:Controller" | "pms:Keyboard" >; export function HumaniseGoalProgress( @@ -226,7 +231,7 @@ export function HumaniseGoalProgress( const gptConfig = GetGamePTConfig(game, playtype); switch (key) { - case "scoreData.gradeIndex": + case "scoreData.gradeIndex": { if (!gptConfig.grades[value]) { throw new Error( `Corrupt goal -- requested a grade of ${value}, which doesn't exist for this game.` @@ -234,7 +239,9 @@ export function HumaniseGoalProgress( } return `${gptConfig.grades[value]} (${userPB?.scoreData.percent.toFixed(2) ?? "0"}%)`; - case "scoreData.lampIndex": + } + + case "scoreData.lampIndex": { if (!gptConfig.lamps[value]) { throw new Error( `Corrupt goal -- requested a lamp of ${value}, which doesn't exist for this game.` @@ -248,6 +255,8 @@ export function HumaniseGoalProgress( } return gptConfig.lamps[value]; + } + case "scoreData.percent": return `${value.toFixed(2)}%`; case "scoreData.score": @@ -363,7 +372,7 @@ export function GetMilestonesThatContainGoal(goalID: string) { */ export async function GetBlockingParentMilestoneSubs( goalSub: GoalSubscriptionDocument -): Promise<(MilestoneSubscriptionDocument & { milestone: MilestoneDocument })[]> { +): Promise> { const blockers = await db["milestone-subs"].aggregate([ { // find all milestones that this user is subscribed to diff --git a/server/src/lib/targets/milestones.ts b/server/src/lib/targets/milestones.ts index d2578a885..526cdb624 100644 --- a/server/src/lib/targets/milestones.ts +++ b/server/src/lib/targets/milestones.ts @@ -1,15 +1,16 @@ +import { EvaluateGoalForUser, SubscribeToGoal } from "./goals"; import db from "external/mongo/db"; import { SubscribeFailReasons } from "lib/constants/err-codes"; import CreateLogCtx from "lib/logger/logger"; import { BulkSendNotification } from "lib/notifications/notifications"; -import { +import type { EvaluatedGoalReturn } from "./goals"; +import type { GoalDocument, GoalSubscriptionDocument, integer, MilestoneDocument, MilestoneSubscriptionDocument, } from "tachi-common"; -import { EvaluatedGoalReturn, EvaluateGoalForUser, SubscribeToGoal } from "./goals"; const logger = CreateLogCtx(__filename); @@ -68,7 +69,7 @@ export function CalculateMilestoneOutOf(milestone: MilestoneDocument) { ); } - return milestone.criteria.value!; + return milestone.criteria.value; } throw new Error( @@ -97,6 +98,7 @@ export async function EvaluateMilestoneProgress(userID: integer, milestone: Mile // If the user is subscribed the milestone, we don't need to calculate // their progress on each goal. const goalSubMap = new Map(); + if (isSubscribedToMilestone) { const goalSubs = await db["goal-subs"].find({ goalID: { $in: goals.map((e) => e.goalID) }, @@ -108,7 +110,7 @@ export async function EvaluateMilestoneProgress(userID: integer, milestone: Mile } } - const goalResults: EvaluatedGoalResult[] = await Promise.all( + const goalResults: Array = await Promise.all( goals.map(async (goal) => { if (isSubscribedToMilestone) { const goalSub = goalSubMap.get(goal.goalID); @@ -171,8 +173,8 @@ export async function EvaluateMilestoneProgress(userID: integer, milestone: Mile interface MilestoneSubscriptionReturns { milestoneSub: MilestoneSubscriptionDocument; - goals: GoalDocument[]; - goalResults: EvaluatedGoalResult[]; + goals: Array; + goalResults: Array; } /** @@ -193,8 +195,8 @@ export async function SubscribeToMilestone( cancelIfAchieved = true ): Promise< | MilestoneSubscriptionReturns - | SubscribeFailReasons.ALREADY_SUBSCRIBED | SubscribeFailReasons.ALREADY_ACHIEVED + | SubscribeFailReasons.ALREADY_SUBSCRIBED > { const isSubscribedToMilestone = await db["milestone-subs"].findOne({ userID, diff --git a/server/src/lib/webhooks/webhooks.ts b/server/src/lib/webhooks/webhooks.ts index 58d6129f2..d8acaf649 100644 --- a/server/src/lib/webhooks/webhooks.ts +++ b/server/src/lib/webhooks/webhooks.ts @@ -1,7 +1,7 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; -import { WebhookEvents } from "tachi-common"; import fetch from "utils/fetch"; +import type { WebhookEvents } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -12,7 +12,7 @@ export async function GetWebhookUrlInfo() { { projection: { webhookUri: 1, clientSecret: 1 } } ); - return urls as { webhookUri: string; clientSecret: string }[]; + return urls as Array<{ webhookUri: string; clientSecret: string }>; } /** diff --git a/server/src/main.ts b/server/src/main.ts index a3de83609..f1b3dc054 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,24 +1,24 @@ // Before we run anything, set a global to indicate to the code that // we're running as a server, and not as a job runner or score worker. -process.env.IS_SERVER = "true"; - -import { spawn } from "child_process"; import db from "external/mongo/db"; import { SetIndexesIfNoneSet } from "external/mongo/indexes"; import { InitSequenceDocs } from "external/mongo/sequence-docs"; -import fs from "fs"; -import http from "http"; -import https from "https"; import { LoadDefaultClients } from "lib/builtin-clients/builtin-clients"; import { VERSION_PRETTY } from "lib/constants/version"; import { HandleSIGTERMGracefully } from "lib/handlers/sigterm"; import CreateLogCtx from "lib/logger/logger"; import { ApplyUnappliedMigrations } from "lib/migration/migrations"; import { Environment, ServerConfig, TachiConfig } from "lib/setup/config"; -import path from "path"; import server from "server/server"; import fetch from "utils/fetch"; import { InitaliseFolderChartLookup } from "utils/folder"; +import { spawn } from "child_process"; +import fs from "fs"; +import https from "https"; +import path from "path"; +import type http from "http"; + +process.env.IS_SERVER = "true"; const logger = CreateLogCtx(__filename); @@ -76,7 +76,9 @@ if (ServerConfig.ENABLE_SERVER_HTTPS) { logger.info(`HTTP Listening on port ${Environment.port}`, { bootInfo: true }); } -process.on("SIGTERM", () => HandleSIGTERMGracefully(instance)); +process.on("SIGTERM", () => { + HandleSIGTERMGracefully(instance); +}); if (process.env.INVOKE_JOB_RUNNER) { logger.info(`Spawning a tachi-server job runner inline.`, { bootInfo: true }); diff --git a/server/src/scripts/clone-and-anonymise-db.ts b/server/src/scripts/clone-and-anonymise-db.ts index 6066660ce..f2db74d8f 100644 --- a/server/src/scripts/clone-and-anonymise-db.ts +++ b/server/src/scripts/clone-and-anonymise-db.ts @@ -1,9 +1,10 @@ -import { execSync } from "child_process"; import { Command } from "commander"; import CreateLogCtx from "lib/logger/logger"; import monk from "monk"; +import { execSync } from "child_process"; const program = new Command(); + program .option("--nsFrom ") .option("--nsTo "); @@ -37,6 +38,7 @@ async function AnonymiseDB(nsTo: string) { $set: { // This is "password" encrypted in bcrypt12. password: "$2b$12$QRFCAxvFoNI2spszFPgt/e.qLy55GvYWlSHioa0AujRbFpChLwHmu", + // emails are uniquely indexed. We need to anonymise these in // a way that they dont duplicate. email: { $concat: [{ $toString: "$userID" }, "@example.com"] }, diff --git a/server/src/scripts/email/send-test-email.ts b/server/src/scripts/email/send-test-email.ts index 0c3b8a8e5..65c1714a9 100644 --- a/server/src/scripts/email/send-test-email.ts +++ b/server/src/scripts/email/send-test-email.ts @@ -4,6 +4,7 @@ import { MainHTMLWrapper } from "lib/email/formats"; import CreateLogCtx from "lib/logger/logger"; const program = new Command(); + program.option("-e, --email "); const logger = CreateLogCtx(__filename); diff --git a/server/src/scripts/iidx-tierlist/get-sp12-data.ts b/server/src/scripts/iidx-tierlist/get-sp12-data.ts index b6f602e16..553b95525 100644 --- a/server/src/scripts/iidx-tierlist/get-sp12-data.ts +++ b/server/src/scripts/iidx-tierlist/get-sp12-data.ts @@ -4,9 +4,9 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; import fetch from "node-fetch"; -import { Difficulties, integer } from "tachi-common"; import { FindChartWithPTDF } from "utils/queries/charts"; import { FindSongOnTitle } from "utils/queries/songs"; +import type { Difficulties, integer } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -23,7 +23,7 @@ interface SP12Data { } async function FetchSP12Data() { - const rj: { sheets: SP12Data[] } = await fetch("https://sp12.iidx.app/api/v1/sheets").then( + const rj: { sheets: Array } = await fetch("https://sp12.iidx.app/api/v1/sheets").then( (r) => r.json() ); // will throw if somethings wrong, anyway @@ -33,6 +33,7 @@ async function FetchSP12Data() { for (const sh of rj.sheets) { let chart; + try { chart = await HumanisedTitleLookup(sh.title); } catch (err) { @@ -48,7 +49,7 @@ async function FetchSP12Data() { let val; switch (key) { - case "n_clear": + case "n_clear": { const v = Math.floor(sh[key] / 2); if (v === 9) { @@ -68,7 +69,9 @@ async function FetchSP12Data() { } break; - case "hard": + } + + case "hard": { const v2 = Math.floor(sh[key] / 2); if (v2 === 9) { @@ -88,8 +91,11 @@ async function FetchSP12Data() { } break; - case "exh": + } + + case "exh": { const v3 = sh[key]; + if (v3 >= 12 || v3 <= 0) { continue; } @@ -97,6 +103,8 @@ async function FetchSP12Data() { val = 12.4 + (12 - v3) * 0.1; break; + } + default: throw new Error("??"); } @@ -147,13 +155,13 @@ async function HumanisedTitleLookup(originalTitle: string) { let title = originalTitle; - if (title.match(/(†|†LEGGENDARIA)$/u)) { + if (/(†|†LEGGENDARIA)$/u.exec(title)) { difficulty = "LEGGENDARIA"; title = title.split(/(†|†LEGGENDARIA)$/u)[0]; - } else if (title.match(/\[H\]$/u)) { + } else if (/\[H\]$/u.exec(title)) { difficulty = "HYPER"; title = title.split("[")[0]; - } else if (title.match(/\[A\]$/u)) { + } else if (/\[A\]$/u.exec(title)) { difficulty = "ANOTHER"; // lmao title = title.split("[")[0]; } diff --git a/server/src/scripts/quick-insert/quick-insert.ts b/server/src/scripts/quick-insert/quick-insert.ts index 93cdfc713..090fb6f5f 100644 --- a/server/src/scripts/quick-insert/quick-insert.ts +++ b/server/src/scripts/quick-insert/quick-insert.ts @@ -1,11 +1,11 @@ import deepmerge from "deepmerge"; -import { ICollection } from "monk"; -import { ChartDocument, IDStrings } from "tachi-common"; import { Random20Hex } from "utils/misc"; +import type { ICollection } from "monk"; +import type { ChartDocument, IDStrings } from "tachi-common"; export async function QuickInsert( base: Omit, "chartID">, - content: Partial>[], + content: Array>>, coll: ICollection ) { const docs = []; diff --git a/server/src/scripts/set-indexes.ts b/server/src/scripts/set-indexes.ts index 0bee47c52..f8170880f 100644 --- a/server/src/scripts/set-indexes.ts +++ b/server/src/scripts/set-indexes.ts @@ -1,6 +1,6 @@ /* eslint-disable no-await-in-loop */ -import { Command } from "commander"; import { SetIndexes } from "../external/mongo/indexes"; +import { Command } from "commander"; const program = new Command(); diff --git a/server/src/scripts/state-sync/recalc-game-profiles.ts b/server/src/scripts/state-sync/recalc-game-profiles.ts index 87bc20540..24446b8c4 100644 --- a/server/src/scripts/state-sync/recalc-game-profiles.ts +++ b/server/src/scripts/state-sync/recalc-game-profiles.ts @@ -30,6 +30,7 @@ export async function CreateGameProfiles() { for (const gpt of gpts) { const { game, playtype } = gpt._id; + logger.info(`Updating ${FormatUserDoc(user)}'s ${game} ${playtype} stats.`); await UpdateUsersGamePlaytypeStats(game, playtype, user.id, null, logger); } diff --git a/server/src/scripts/state-sync/sync-state.ts b/server/src/scripts/state-sync/sync-state.ts index 9ca4c6224..320ae3a44 100644 --- a/server/src/scripts/state-sync/sync-state.ts +++ b/server/src/scripts/state-sync/sync-state.ts @@ -1,7 +1,7 @@ +import { CreateGameProfiles } 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"; -import { CreateGameProfiles } from "./recalc-game-profiles"; const logger = CreateLogCtx(__filename); diff --git a/server/src/scripts/sync-database.ts b/server/src/scripts/sync-database.ts index 55e914467..78a18dc1d 100644 --- a/server/src/scripts/sync-database.ts +++ b/server/src/scripts/sync-database.ts @@ -4,14 +4,19 @@ import db, { monkDB } from "external/mongo/db"; import fjsh from "fast-json-stable-hash"; import { PullDatabaseSeeds } from "lib/database-seeds/repo"; -import CreateLogCtx, { KtLogger } from "lib/logger/logger"; +import CreateLogCtx from "lib/logger/logger"; import UpdateIsPrimaryStatus from "lib/score-mutation/update-isprimary"; import { TachiConfig } from "lib/setup/config"; import { RemoveStaleFolderShowcaseStats } from "lib/showcase/showcase"; import { UpdateMilestoneSubscriptions } from "lib/targets/milestones"; -import { BulkWriteOperation } from "mongodb"; -import { ICollection } from "monk"; -import { +import { RecalcAllScores } from "utils/calculations/recalc-scores"; +import { UpdateGameSongIDCounter } from "utils/db"; +import { InitaliseFolderChartLookup } from "utils/folder"; +import { ArrayDiff, IsSupported } from "utils/misc"; +import type { KtLogger } from "lib/logger/logger"; +import type { BulkWriteOperation } from "mongodb"; +import type { ICollection } from "monk"; +import type { BMSCourseDocument, ChartDocument, FolderDocument, @@ -22,31 +27,28 @@ import { SongDocument, TableDocument, } from "tachi-common"; -import { RecalcAllScores } from "utils/calculations/recalc-scores"; -import { UpdateGameSongIDCounter } from "utils/db"; -import { InitaliseFolderChartLookup } from "utils/folder"; -import { ArrayDiff, IsSupported } from "utils/misc"; interface SyncInstructions { pattern: RegExp; handler: ( // These 'any's are necessary because generifying here kinda sucks. // eslint-disable-next-line @typescript-eslint/no-explicit-any - c: any[], + c: Array, // eslint-disable-next-line @typescript-eslint/no-explicit-any - collection: ICollection, + collection: ICollection, logger: KtLogger, collectionName: string ) => Promise; } async function RemoveNotPresent( - documents: T[], + documents: Array, collection: ICollection, field: keyof T, logger: KtLogger ) { logger.verbose(`Removing all documents that are no longer present.`); + // Remove anything no longer present. // Note that $nin is incredibly slow. // @ts-expect-error generic system failing @@ -62,7 +64,7 @@ async function RemoveNotPresent( } async function GenericUpsert( - documents: T[], + documents: Array, collection: ICollection, field: keyof T, logger: KtLogger, @@ -71,11 +73,12 @@ async function GenericUpsert( ) { logger.verbose(`Running bulkwrite.`); - const bwriteOps: BulkWriteOperation[] = []; + const bwriteOps: Array> = []; const allExistingDocs = await collection.find({}); const map = new Map(); + for (const doc of allExistingDocs) { map.set(doc[field], doc); } @@ -83,6 +86,7 @@ async function GenericUpsert( const changedFields = []; let i = 0; + for (const document of documents) { i++; if (i % 10_000 === 0) { @@ -139,11 +143,11 @@ async function GenericUpsert( }; } -const syncInstructions: SyncInstructions[] = [ +const syncInstructions: Array = [ { pattern: /^charts-(b|p)ms/u, handler: async ( - charts: ChartDocument[], + charts: Array, collection: ICollection, logger, collectionName @@ -165,7 +169,7 @@ const syncInstructions: SyncInstructions[] = [ { pattern: /^charts-/u, handler: async ( - charts: ChartDocument[], + charts: Array, collection: ICollection, logger ) => { @@ -183,7 +187,11 @@ const syncInstructions: SyncInstructions[] = [ }, { pattern: /^songs-(b|p)ms/u, - handler: async (songs: SongDocument[], collection: ICollection, logger) => { + handler: async ( + songs: Array, + collection: ICollection, + logger + ) => { const r = await GenericUpsert(songs, collection, "id", logger, false); if (r.thingsChanged) { @@ -195,7 +203,11 @@ const syncInstructions: SyncInstructions[] = [ }, { pattern: /^songs-/u, - handler: async (songs: SongDocument[], collection: ICollection, logger) => { + handler: async ( + songs: Array, + collection: ICollection, + logger + ) => { const r = await GenericUpsert(songs, collection, "id", logger, true); if (r.thingsChanged) { @@ -208,7 +220,7 @@ const syncInstructions: SyncInstructions[] = [ { pattern: /^folders/u, handler: async ( - folders: FolderDocument[], + folders: Array, collection: ICollection, logger ) => { @@ -223,7 +235,7 @@ const syncInstructions: SyncInstructions[] = [ if (r) { await InitaliseFolderChartLookup(); - const allModifiedFolderIDs = r.changedFields as string[]; + const allModifiedFolderIDs = r.changedFields as Array; const keptFolderIDs = await db.folders.find( { @@ -245,7 +257,7 @@ const syncInstructions: SyncInstructions[] = [ }, { pattern: /^tables/u, - handler: (tables: TableDocument[], collection: ICollection, logger) => + handler: (tables: Array, collection: ICollection, logger) => GenericUpsert( tables.filter((e) => TachiConfig.GAMES.includes(e.game)), collection, @@ -257,7 +269,7 @@ const syncInstructions: SyncInstructions[] = [ { pattern: /^bms-course-lookup/u, handler: async ( - bmsCourseDocuments: BMSCourseDocument[], + bmsCourseDocuments: Array, collection: ICollection, logger ) => { @@ -270,7 +282,11 @@ const syncInstructions: SyncInstructions[] = [ }, { pattern: /^goals/u, - handler: async (goals: GoalDocument[], collection: ICollection, logger) => { + handler: async ( + goals: Array, + collection: ICollection, + logger + ) => { // never remove goals. Never update goals either. Only insert new ones as // they come in. await GenericUpsert( @@ -286,7 +302,7 @@ const syncInstructions: SyncInstructions[] = [ { pattern: /^milestone-sets/u, handler: async ( - milestoneSets: MilestoneSetDocument[], + milestoneSets: Array, collection: ICollection, logger ) => { @@ -302,7 +318,7 @@ const syncInstructions: SyncInstructions[] = [ { pattern: /^milestones/u, handler: async ( - milestones: MilestoneDocument[], + milestones: Array, collection: ICollection, logger ) => { @@ -315,7 +331,7 @@ const syncInstructions: SyncInstructions[] = [ ); if (r.thingsChanged) { - const affectedMilestoneIDs = r.changedFields as string[]; + const affectedMilestoneIDs = r.changedFields as Array; await Promise.all(affectedMilestoneIDs.map((e) => UpdateMilestoneSubscriptions(e))); } @@ -348,8 +364,9 @@ async function SynchroniseDBWithSeeds() { spawnLogger.verbose(`Found ${data.length} documents.`); let matchedSomething = false; + for (const syncInst of syncInstructions) { - if (collectionName.match(syncInst.pattern)) { + if (syncInst.pattern.exec(collectionName)) { spawnLogger.info(`Starting handler...`); await syncInst.handler( data, diff --git a/server/src/scripts/validate-database.ts b/server/src/scripts/validate-database.ts index d08e26167..3da8f02af 100644 --- a/server/src/scripts/validate-database.ts +++ b/server/src/scripts/validate-database.ts @@ -1,13 +1,15 @@ // note: a hell of a lot of this code is copy pasted // this is just a script, so, feel free to refactor it. import { Command } from "commander"; -import { Databases, monkDB } from "external/mongo/db"; +import { monkDB } from "external/mongo/db"; import { DatabaseSchemas } from "external/mongo/schemas"; import CreateLogCtx from "lib/logger/logger"; -import { PrudenceError } from "prudence"; import { FormatPrError } from "utils/prudence"; +import type { Databases } from "external/mongo/db"; +import type { PrudenceError } from "prudence"; const program = new Command(); + program.option("-c, --collection "); program.parse(process.argv); @@ -22,7 +24,7 @@ export async function ValidateCollection(collectionName: Databases): Promise { try { diff --git a/server/src/server/middleware/auth.test.ts b/server/src/server/middleware/auth.test.ts index b114619dd..518a784fa 100644 --- a/server/src/server/middleware/auth.test.ts +++ b/server/src/server/middleware/auth.test.ts @@ -1,6 +1,6 @@ import expMiddlewareMock from "express-request-mock"; import db from "external/mongo/db"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import { ALL_PERMISSIONS } from "tachi-common"; import t from "tap"; import mockApi from "test-utils/mock-api"; @@ -27,7 +27,7 @@ t.test("#SetRequestPermissions", (t) => { }, }); - t.strictSame(req[SYMBOL_TachiAPIAuth], { + t.strictSame(req[SYMBOL_TACHI_API_AUTH], { userID: 1, identifier: "Mock API Token", permissions: { @@ -43,7 +43,7 @@ t.test("#SetRequestPermissions", (t) => { t.test("Should assign guest APIKey information if no auth present", async (t) => { const { req } = await expMiddlewareMock(SetRequestPermissions); - t.strictSame(req[SYMBOL_TachiAPIAuth], { + t.strictSame(req[SYMBOL_TACHI_API_AUTH], { userID: null, identifier: "Guest Token", permissions: {}, @@ -109,7 +109,7 @@ t.test("#SetRequestPermissions", (t) => { }, }); - t.strictSame(req[SYMBOL_TachiAPIAuth], { + t.strictSame(req[SYMBOL_TACHI_API_AUTH], { userID: 1, identifier: `Session-Key 1`, token: null, diff --git a/server/src/server/middleware/auth.ts b/server/src/server/middleware/auth.ts index cbd317d92..16648716a 100644 --- a/server/src/server/middleware/auth.ts +++ b/server/src/server/middleware/auth.ts @@ -1,10 +1,11 @@ -import { RequestHandler } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { TachiConfig } from "lib/setup/config"; -import { APIPermissions, APITokenDocument, UserAuthLevels, ALL_PERMISSIONS } from "tachi-common"; +import { UserAuthLevels, ALL_PERMISSIONS } from "tachi-common"; import { SplitAuthorizationHeader } from "utils/misc"; +import type { RequestHandler } from "express"; +import type { APIPermissions, APITokenDocument } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -34,23 +35,25 @@ export const SetRequestPermissions: RequestHandler = CreateSetRequestPermissions */ function CreateSetRequestPermissions(errorKeyName: string): RequestHandler { return async (req, res, next) => { - if (req.session?.tachi?.user.id) { - req[SYMBOL_TachiAPIAuth] = { + if (req.session.tachi?.user.id) { + req[SYMBOL_TACHI_API_AUTH] = { userID: req.session.tachi.user.id, identifier: `Session-Key ${req.session.tachi.user.id}`, token: null, permissions: ALL_PERMISSIONS, fromAPIClient: null, }; - return next(); + next(); + return; } const header = req.header("Authorization"); // if no auth was attempted, default to the guest token. if (!header) { - req[SYMBOL_TachiAPIAuth] = GuestToken; - return next(); + req[SYMBOL_TACHI_API_AUTH] = GuestToken; + next(); + return; } const { token, type } = SplitAuthorizationHeader(header); @@ -81,7 +84,7 @@ function CreateSetRequestPermissions(errorKeyName: string): RequestHandler { }); } - req[SYMBOL_TachiAPIAuth] = { + req[SYMBOL_TACHI_API_AUTH] = { userID: apiTokenData.userID, token, permissions: apiTokenData.permissions, @@ -89,7 +92,7 @@ function CreateSetRequestPermissions(errorKeyName: string): RequestHandler { fromAPIClient: apiTokenData.fromAPIClient, }; - return next(); + next(); }; } @@ -108,9 +111,9 @@ export const SetFervidexStyleRequestPermissions: RequestHandler = * @returns A middleware function. */ export const RequirePermissions = - (...perms: APIPermissions[]): RequestHandler => + (...perms: Array): RequestHandler => (req, res, next) => { - if (!req[SYMBOL_TachiAPIAuth]) { + if (!req[SYMBOL_TACHI_API_AUTH]) { logger.error( `RequirePermissions middleware was hit without any TachiAPIAuthentication?` ); @@ -121,7 +124,7 @@ export const RequirePermissions = }); } - if (!req[SYMBOL_TachiAPIAuth].userID) { + if (!req[SYMBOL_TACHI_API_AUTH].userID) { return res.status(401).json({ success: false, description: `You are not authorised to perform this action.`, @@ -129,8 +132,9 @@ export const RequirePermissions = } const missingPerms = []; + for (const perm of perms) { - if (!req[SYMBOL_TachiAPIAuth]!.permissions[perm]) { + if (!req[SYMBOL_TACHI_API_AUTH]!.permissions[perm]) { missingPerms.push(perm); } } @@ -138,7 +142,7 @@ export const RequirePermissions = if (missingPerms.length > 0) { logger.info( `IP ${req.ip} - userID ${ - req[SYMBOL_TachiAPIAuth].userID + req[SYMBOL_TACHI_API_AUTH].userID } had insufficient permissions for request ${req.method} ${ req.url }. ${missingPerms.join(", ")}` @@ -151,13 +155,13 @@ export const RequirePermissions = }); } - return next(); + next(); }; const CreateRequireNotGuest = (errorKeyName: string): RequestHandler => (req, res, next) => { - if (!req[SYMBOL_TachiAPIAuth]) { + if (!req[SYMBOL_TACHI_API_AUTH]) { logger.error(`RequirePermissions middleware was hit without any TachiAPIData?`); return res.status(500).json({ success: false, @@ -165,7 +169,7 @@ const CreateRequireNotGuest = }); } - if (req[SYMBOL_TachiAPIAuth].userID === null) { + if (req[SYMBOL_TACHI_API_AUTH].userID === null) { logger.info(`Request to ${req.method} ${req.url} was attempted by guest.`); return res.status(401).json({ success: false, @@ -173,7 +177,7 @@ const CreateRequireNotGuest = }); } - return next(); + next(); }; export const RequireNotGuest: RequestHandler = CreateRequireNotGuest("description"); @@ -181,9 +185,9 @@ export const RequireNotGuest: RequestHandler = CreateRequireNotGuest("descriptio export const FervidexStyleRequireNotGuest: RequestHandler = CreateRequireNotGuest("error"); export const RejectIfBanned: RequestHandler = async (req, res, next) => { - if (req[SYMBOL_TachiAPIAuth].userID) { + if (req[SYMBOL_TACHI_API_AUTH].userID) { const isBanned = await db.users.findOne({ - id: req[SYMBOL_TachiAPIAuth].userID!, + id: req[SYMBOL_TACHI_API_AUTH].userID!, authLevel: UserAuthLevels.BANNED, }); diff --git a/server/src/server/middleware/multer-upload.ts b/server/src/server/middleware/multer-upload.ts index d852394e5..71d7423df 100644 --- a/server/src/server/middleware/multer-upload.ts +++ b/server/src/server/middleware/multer-upload.ts @@ -1,10 +1,10 @@ // @todo #118 -import { RequestHandler } from "express"; import { SIXTEEN_MEGABTYES } from "lib/constants/filesize"; import CreateLogCtx from "lib/logger/logger"; import multer, { MulterError } from "multer"; -import { integer } from "tachi-common"; +import type { RequestHandler } from "express"; +import type { integer } from "tachi-common"; const defaultLogger = CreateLogCtx(__filename); @@ -44,7 +44,7 @@ export const CreateMulterSingleUploadMiddleware = ( }); } - return next(); + next(); }); }; }; diff --git a/server/src/server/middleware/prudence-validate.ts b/server/src/server/middleware/prudence-validate.ts index c65020a6d..688b659ec 100644 --- a/server/src/server/middleware/prudence-validate.ts +++ b/server/src/server/middleware/prudence-validate.ts @@ -1,6 +1,7 @@ -import { RequestHandler } from "express-serve-static-core"; import CreateLogCtx from "lib/logger/logger"; -import Prudence, { +import Prudence from "prudence"; +import type { RequestHandler } from "express-serve-static-core"; +import type { ErrorMessages, MiddlewareErrorHandler, PrudenceOptions, @@ -16,6 +17,7 @@ const API_ERR_HANDLER = (logLevel: TachiLogLevels): MiddlewareErrorHandler => (req, res, next, error) => { let stringVal = error.userVal; + if (error.keychain && error.keychain.includes("password") && error.userVal) { stringVal = "****"; } @@ -51,7 +53,7 @@ const API_ERROR_HANDLERS = Object.fromEntries( ]) ) as Record; -type TachiLogLevels = "crit" | "severe" | "error" | "warn" | "info" | "verbose" | "debug"; +type TachiLogLevels = "crit" | "debug" | "error" | "info" | "severe" | "verbose" | "warn"; const prValidate = ( s: PrudenceSchema, diff --git a/server/src/server/middleware/rate-limiter.ts b/server/src/server/middleware/rate-limiter.ts index b361357ab..a8ff3597d 100644 --- a/server/src/server/middleware/rate-limiter.ts +++ b/server/src/server/middleware/rate-limiter.ts @@ -1,12 +1,13 @@ -import { Request } from "express"; -import rateLimit, { Options } from "express-rate-limit"; +import rateLimit from "express-rate-limit"; import { RedisClient } from "external/redis/redis"; import { ONE_MINUTE } from "lib/constants/time"; import CreateLogCtx from "lib/logger/logger"; import { Environment, ServerConfig, TachiConfig } from "lib/setup/config"; import RateLimitRedis from "rate-limit-redis"; -import { integer } from "tachi-common"; import { OmitUndefinedKeys } from "utils/misc"; +import type { Request } from "express"; +import type { Options } from "express-rate-limit"; +import type { integer } from "tachi-common"; const logger = CreateLogCtx(__filename); diff --git a/server/src/server/middleware/request-logger.ts b/server/src/server/middleware/request-logger.ts index 6bbb2c58a..11b8c7c58 100644 --- a/server/src/server/middleware/request-logger.ts +++ b/server/src/server/middleware/request-logger.ts @@ -1,7 +1,7 @@ -import { RequestHandler, Response } from "express-serve-static-core"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { TachiConfig } from "lib/setup/config"; +import type { RequestHandler, Response } from "express-serve-static-core"; const logger = CreateLogCtx(__filename); @@ -51,7 +51,7 @@ export const RequestLoggerMiddleware: RequestHandler = (req, res, next) => { statusCode: res.statusCode, requestQuery: req.query, requestBody: safeBody, - from: req[SYMBOL_TachiAPIAuth]?.userID ?? null, + from: req[SYMBOL_TACHI_API_AUTH].userID ?? null, fromIp: req.ip, }; @@ -80,5 +80,5 @@ export const RequestLoggerMiddleware: RequestHandler = (req, res, next) => { } }); - return next(); + next(); }; diff --git a/server/src/server/middleware/sanitise-qstring.ts b/server/src/server/middleware/sanitise-qstring.ts index 6b7cc3603..ce78ce57f 100644 --- a/server/src/server/middleware/sanitise-qstring.ts +++ b/server/src/server/middleware/sanitise-qstring.ts @@ -1,4 +1,4 @@ -import { NextFunction, Request, Response } from "express"; +import type { NextFunction, Request, Response } from "express"; /** * Disallow complex query strings by enforcing single key:value pairs. diff --git a/server/src/server/middleware/type-require.ts b/server/src/server/middleware/type-require.ts index 7e0662837..283f9eaf6 100644 --- a/server/src/server/middleware/type-require.ts +++ b/server/src/server/middleware/type-require.ts @@ -1,5 +1,5 @@ -import { RequestHandler } from "express"; import { TachiConfig } from "lib/setup/config"; +import type { RequestHandler } from "express"; /** * Middleware that makes the route only available under Bokutachi. @@ -8,7 +8,8 @@ import { TachiConfig } from "lib/setup/config"; */ export const RequireBokutachi: RequestHandler = (req, res, next) => { if (TachiConfig.TYPE === "btchi" || TachiConfig.TYPE === "omni") { - return next(); + next(); + return; } return res.status(404).send({ @@ -24,7 +25,8 @@ export const RequireBokutachi: RequestHandler = (req, res, next) => { */ export const RequireKamaitachi: RequestHandler = (req, res, next) => { if (TachiConfig.TYPE === "ktchi" || TachiConfig.TYPE === "omni") { - return next(); + next(); + return; } return res.status(404).send({ diff --git a/server/src/server/middleware/update-last-seen.ts b/server/src/server/middleware/update-last-seen.ts index 9654a8f63..b9395860f 100644 --- a/server/src/server/middleware/update-last-seen.ts +++ b/server/src/server/middleware/update-last-seen.ts @@ -1,13 +1,15 @@ -import { RequestHandler } from "express"; import db from "external/mongo/db"; +import type { RequestHandler } from "express"; export const UpdateLastSeen: RequestHandler = (req, res, next) => { if (!req.session.tachi?.user.id) { - return next(); + next(); + return; } if (req.session.tachi.settings.preferences.invisible) { - return next(); + next(); + return; } // fire, but we have no reason to await it. @@ -20,5 +22,5 @@ export const UpdateLastSeen: RequestHandler = (req, res, next) => { } ); - return next(); + next(); }; diff --git a/server/src/server/router/api/v1/admin/router.ts b/server/src/server/router/api/v1/admin/router.ts index fc0c6f750..40bb3468f 100644 --- a/server/src/server/router/api/v1/admin/router.ts +++ b/server/src/server/router/api/v1/admin/router.ts @@ -1,36 +1,38 @@ /* eslint-disable no-await-in-loop */ -import { RequestHandler, Router } from "express"; -import p from "prudence"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; -import CreateLogCtx, { ChangeRootLogLevel, GetLogLevel } from "lib/logger/logger"; -import prValidate from "server/middleware/prudence-validate"; -import { GetUserWithID } from "utils/user"; -import { ONE_MINUTE } from "lib/constants/time"; -import { ServerConfig, TachiConfig } from "lib/setup/config"; -import { Game, UserAuthLevels } from "tachi-common"; +import { Router } from "express"; import db from "external/mongo/db"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; +import { ONE_MINUTE } from "lib/constants/time"; +import CreateLogCtx, { ChangeRootLogLevel, GetLogLevel } from "lib/logger/logger"; import { DeleteMultipleScores, DeleteScore } from "lib/score-mutation/delete-scores"; +import { ServerConfig, TachiConfig } from "lib/setup/config"; +import p from "prudence"; +import prValidate from "server/middleware/prudence-validate"; +import { UserAuthLevels } from "tachi-common"; import { RecalcAllScores, UpdateAllPBs } from "utils/calculations/recalc-scores"; -import DestroyUserGamePlaytypeData from "utils/reset-state/destroy-ugpt"; import { RecalcSessions } from "utils/calculations/recalc-sessions"; +import DestroyUserGamePlaytypeData from "utils/reset-state/destroy-ugpt"; +import { GetUserWithID } from "utils/user"; +import type { RequestHandler } from "express"; +import type { Game } from "tachi-common"; const logger = CreateLogCtx(__filename); const router: Router = Router({ mergeParams: true }); const RequireAdminLevel: RequestHandler = async (req, res, next) => { - if (!req[SYMBOL_TachiAPIAuth].userID) { + if (!req[SYMBOL_TACHI_API_AUTH].userID) { return res.status(401).json({ success: false, description: `You are not authenticated.`, }); } - const userDoc = await GetUserWithID(req[SYMBOL_TachiAPIAuth].userID!); + const userDoc = await GetUserWithID(req[SYMBOL_TACHI_API_AUTH].userID!); if (!userDoc) { logger.severe( - `Api Token ${req[SYMBOL_TachiAPIAuth].token} is assigned to ${req[SYMBOL_TachiAPIAuth].userID}, who does not exist?` + `Api Token ${req[SYMBOL_TACHI_API_AUTH].token} is assigned to ${req[SYMBOL_TACHI_API_AUTH].userID}, who does not exist?` ); return res.status(500).json({ @@ -46,7 +48,7 @@ const RequireAdminLevel: RequestHandler = async (req, res, next) => { }); } - return next(); + next(); }; const LOG_LEVEL = ServerConfig.LOGGER_CONFIG.LOG_LEVEL; @@ -74,6 +76,7 @@ router.post( }), (req, res) => { const logLevel = GetLogLevel(); + ChangeRootLogLevel(req.body.logLevel); const duration = req.body.duration ?? 60; @@ -240,6 +243,7 @@ router.post( */ router.post("/recalc", async (req, res) => { const filter = req.body ?? {}; + await RecalcAllScores(filter); const scoreIDs = ( diff --git a/server/src/server/router/api/v1/auth/auth.ts b/server/src/server/router/api/v1/auth/auth.ts index ea721356b..5bf22112e 100644 --- a/server/src/server/router/api/v1/auth/auth.ts +++ b/server/src/server/router/api/v1/auth/auth.ts @@ -2,12 +2,13 @@ import bcrypt from "bcryptjs"; import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; import { Environment, ServerConfig } from "lib/setup/config"; -import { integer, PublicUserDocument, UserAuthLevels, UserSettings } from "tachi-common"; +import { UserAuthLevels } from "tachi-common"; import nodeFetch from "utils/fetch"; import { Random20Hex } from "utils/misc"; -import { PrivateUserInfoDocument } from "utils/types"; import { CreateURLWithParams } from "utils/url"; import { FormatUserDoc } from "utils/user"; +import type { integer, PublicUserDocument, UserSettings } from "tachi-common"; +import type { PrivateUserInfoDocument } from "utils/types"; const logger = CreateLogCtx(__filename); diff --git a/server/src/server/router/api/v1/auth/router.ts b/server/src/server/router/api/v1/auth/router.ts index 9eeeea0fb..66a56ea8b 100644 --- a/server/src/server/router/api/v1/auth/router.ts +++ b/server/src/server/router/api/v1/auth/router.ts @@ -1,3 +1,13 @@ +import { + AddNewUser, + HashPassword, + InsertDefaultUserSettings, + MountAuthCookie, + PasswordCompare, + ReinstateInvite, + ValidateCaptcha, + ValidatePassword, +} from "./auth"; import { Router } from "express"; import db from "external/mongo/db"; import { SendEmail } from "lib/email/client"; @@ -10,7 +20,6 @@ import { AggressiveRateLimitMiddleware, HyperAggressiveRateLimitMiddleware, } from "server/middleware/rate-limiter"; -import { integer } from "tachi-common"; import { DecrementCounterValue, GetNextCounterValue } from "utils/db"; import { Random20Hex } from "utils/misc"; import { @@ -21,16 +30,7 @@ import { GetUserPrivateInfo, GetUserWithID, } from "utils/user"; -import { - AddNewUser, - HashPassword, - InsertDefaultUserSettings, - MountAuthCookie, - PasswordCompare, - ReinstateInvite, - ValidateCaptcha, - ValidatePassword, -} from "./auth"; +import type { integer } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -272,7 +272,7 @@ router.post( await db["verify-email-codes"].insert({ code: resetEmailCode, - userID: userID, + userID, email: req.body.email, }); @@ -366,13 +366,14 @@ router.post("/resend-verify-email", HyperAggressiveRateLimitMiddleware, async (r }); const user = req.session.tachi?.user; + if (!user) { return; } const userID = user.id; - const verifyInfo = await db["verify-email-codes"].findOne({ userID: userID }); + const verifyInfo = await db["verify-email-codes"].findOne({ userID }); if (!verifyInfo) { logger.warn( @@ -393,7 +394,7 @@ router.post("/resend-verify-email", HyperAggressiveRateLimitMiddleware, async (r * @name POST /api/v1/auth/logout */ router.post("/logout", (req, res) => { - if (!req.session?.tachi?.user.id) { + if (!req.session.tachi?.user.id) { return res.status(409).json({ success: false, description: `You are not logged in.`, @@ -430,6 +431,7 @@ router.post( } logger.debug(`received password reset request for ${req.body.email}.`); + // For timing attack and infosec reasons, we can't do anything but **immediately** return here. res.status(202).json({ success: true, 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 e86217167..7af19c1f6 100644 --- a/server/src/server/router/api/v1/clients/middleware.test.ts +++ b/server/src/server/router/api/v1/clients/middleware.test.ts @@ -1,5 +1,5 @@ import expMiddlewareMock from "express-request-mock"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import t from "tap"; import ResetDBState from "test-utils/resets"; import { GetClientFromID, RequireOwnershipOfClient } from "./middleware"; @@ -15,7 +15,7 @@ t.test("#GetClientFromID", (t) => { }); t.strictSame( - req[SYMBOL_TachiData]?.apiClientDoc, + req[SYMBOL_TACHI_DATA]?.apiClientDoc, { clientID: "OAUTH2_CLIENT_ID", // clientSecret: "OAUTH2_CLIENT_SECRET", diff --git a/server/src/server/router/api/v1/clients/middleware.ts b/server/src/server/router/api/v1/clients/middleware.ts index 3c8d6b02d..e5f4dd718 100644 --- a/server/src/server/router/api/v1/clients/middleware.ts +++ b/server/src/server/router/api/v1/clients/middleware.ts @@ -1,9 +1,9 @@ -import { RequestHandler } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { Environment } from "lib/setup/config"; -import { TachiAPIClientDocument } from "tachi-common"; import { AssignToReqTachiData } from "utils/req-tachi-data"; +import type { RequestHandler } from "express"; +import type { TachiAPIClientDocument } from "tachi-common"; export const GetClientFromID: RequestHandler = async (req, res, next) => { const client = await db["api-clients"].findOne( @@ -26,7 +26,7 @@ export const GetClientFromID: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { apiClientDoc: client }); - return next(); + next(); }; export const RequireOwnershipOfClient: RequestHandler = (req, res, next) => { @@ -42,7 +42,7 @@ export const RequireOwnershipOfClient: RequestHandler = (req, res, next) => { // in testing. client = req.body.__terribleHackOauth2ClientDoc; } else { - client = req[SYMBOL_TachiData]!.apiClientDoc!; + client = req[SYMBOL_TACHI_DATA]!.apiClientDoc!; } const user = req.session.tachi?.user; @@ -60,5 +60,6 @@ export const RequireOwnershipOfClient: RequestHandler = (req, res, next) => { description: `You are not authorized to perform this action.`, }); } - return next(); + + next(); }; diff --git a/server/src/server/router/api/v1/clients/router.ts b/server/src/server/router/api/v1/clients/router.ts index d759b86fc..39184f8b0 100644 --- a/server/src/server/router/api/v1/clients/router.ts +++ b/server/src/server/router/api/v1/clients/router.ts @@ -1,20 +1,16 @@ +import { GetClientFromID, RequireOwnershipOfClient } from "./middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { ServerConfig } from "lib/setup/config"; import p from "prudence"; import prValidate from "server/middleware/prudence-validate"; -import { - ALL_PERMISSIONS, - APIPermissions, - TachiAPIClientDocument, - UserAuthLevels, -} from "tachi-common"; +import { ALL_PERMISSIONS, UserAuthLevels } from "tachi-common"; import { DedupeArr, DeleteUndefinedProps, IsValidURL, Random20Hex } from "utils/misc"; import { optNull } from "utils/prudence"; import { FormatUserDoc } from "utils/user"; -import { GetClientFromID, RequireOwnershipOfClient } from "./middleware"; +import type { APIPermissions, TachiAPIClientDocument } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -168,7 +164,7 @@ router.post( * @name GET /api/v1/clients/:clientID */ router.get("/:clientID", GetClientFromID, (req, res) => { - const client = req[SYMBOL_TachiData]!.apiClientDoc!; + const client = req[SYMBOL_TACHI_DATA]!.apiClientDoc!; return res.status(200).json({ success: true, @@ -211,6 +207,7 @@ router.patch( if (typeof self !== "string") { return "Expected a string."; } + const res = IsValidURL(self); if (!res) { @@ -223,6 +220,7 @@ router.patch( if (typeof self !== "string") { return "Expected a string."; } + const res = IsValidURL(self); if (!res) { @@ -233,7 +231,7 @@ router.patch( }), }), async (req, res) => { - const client = req[SYMBOL_TachiData]!.apiClientDoc!; + const client = req[SYMBOL_TACHI_DATA]!.apiClientDoc!; DeleteUndefinedProps(req.body); @@ -276,7 +274,7 @@ router.post( GetClientFromID, RequireOwnershipOfClient, async (req, res) => { - const client = req[SYMBOL_TachiData]!.apiClientDoc!; + const client = req[SYMBOL_TACHI_DATA]!.apiClientDoc!; const clientName = `${client.name} (${client.clientID})`; logger.info(`received request to reset client secret for ${clientName}`); @@ -308,7 +306,7 @@ router.post( * @name DELETE /api/v1/clients/:clientID */ router.delete("/:clientID", GetClientFromID, RequireOwnershipOfClient, async (req, res) => { - const client = req[SYMBOL_TachiData]!.apiClientDoc!; + const client = req[SYMBOL_TACHI_DATA]!.apiClientDoc!; const clientName = `${client.name} (${client.clientID})`; @@ -324,6 +322,7 @@ router.delete("/:clientID", GetClientFromID, RequireOwnershipOfClient, async (re const result = await db["api-tokens"].remove({ fromOAuth2Client: client.clientID, }); + logger.info(`Removed ${result.deletedCount} api tokens from ${clientName}.`); return res.status(200).json({ 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 b823d6c54..6ece357c8 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 @@ -1,6 +1,6 @@ -import { RequestHandler } from "express"; import db from "external/mongo/db"; import { AssignToReqTachiData, GetGPT } from "utils/req-tachi-data"; +import type { RequestHandler } from "express"; export const ValidateAndGetChart: RequestHandler = async (req, res, next) => { const { game, playtype } = GetGPT(req); @@ -21,5 +21,5 @@ export const ValidateAndGetChart: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { chartDoc: chart }); - return next(); + next(); }; 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 9ccb990dc..ee173ea8b 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 @@ -1,14 +1,15 @@ +import { ValidateAndGetChart } from "./middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { SearchUsersRegExp } from "lib/search/search"; -import { FilterQuery } from "mongodb"; -import { FolderDocument, FormatChart } from "tachi-common"; +import { FormatChart } from "tachi-common"; import { IsString } from "utils/misc"; import { ParseStrPositiveNonZeroInt } from "utils/string-checks"; import { GetUsersWithIDs } from "utils/user"; -import { ValidateAndGetChart } from "./middleware"; +import type { FilterQuery } from "mongodb"; +import type { FolderDocument } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -22,8 +23,8 @@ router.use(ValidateAndGetChart); * @name GET /api/v1/games/:game/:playtype/charts/:chartID */ router.get("/", async (req, res) => { - const chart = req[SYMBOL_TachiData]!.chartDoc!; - const game = req[SYMBOL_TachiData]!.game!; + const chart = req[SYMBOL_TACHI_DATA]!.chartDoc!; + const game = req[SYMBOL_TACHI_DATA]!.game!; const song = await db.songs[game].findOne({ id: chart.songID, @@ -58,7 +59,7 @@ router.get("/", async (req, res) => { * @name GET /api/v1/games/:game/:playtype/charts/:chartID/folders */ router.get("/folders", async (req, res) => { - const chart = req[SYMBOL_TachiData]!.chartDoc!; + const chart = req[SYMBOL_TACHI_DATA]!.chartDoc!; const folderIDs = await db["folder-chart-lookup"].find( { @@ -94,7 +95,7 @@ router.get("/folders", async (req, res) => { * @name GET /api/v1/games/:game/:playtype/charts/:chartID/playcount */ router.get("/playcount", async (req, res) => { - const chart = req[SYMBOL_TachiData]!.chartDoc!; + const chart = req[SYMBOL_TACHI_DATA]!.chartDoc!; const count = await db["personal-bests"].count({ chartID: chart.chartID }); @@ -116,7 +117,7 @@ router.get("/playcount", async (req, res) => { * @name GET /api/v1/games/:game/:playtype/charts/:chartID/pbs */ router.get("/pbs", async (req, res) => { - const chart = req[SYMBOL_TachiData]!.chartDoc!; + const chart = req[SYMBOL_TACHI_DATA]!.chartDoc!; const startRanking = ParseStrPositiveNonZeroInt(req.query.startRanking) ?? 1; @@ -153,7 +154,7 @@ router.get("/pbs", async (req, res) => { * @name GET /api/v1/games/:game/:playtype/charts/:chartID/pbs/search */ router.get("/pbs/search", async (req, res) => { - const chart = req[SYMBOL_TachiData]!.chartDoc!; + const chart = req[SYMBOL_TACHI_DATA]!.chartDoc!; if (!IsString(req.query.search)) { return res.status(400).json({ 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 c77db0a58..e08b29be0 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 @@ -1,12 +1,12 @@ +import chartIDRouter from "./_chartID/router"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import { SearchGameSongs } from "lib/search/search"; -import { ChartDocument, UGPTSettings } from "tachi-common"; import { IsString } from "utils/misc"; import { FindChartsOnPopularity } from "utils/queries/charts"; import { GetGPT } from "utils/req-tachi-data"; -import chartIDRouter from "./_chartID/router"; +import type { ChartDocument, UGPTSettings } from "tachi-common"; const router: Router = Router({ mergeParams: true }); @@ -23,10 +23,11 @@ const router: Router = Router({ mergeParams: true }); router.get("/", async (req, res) => { const { game, playtype } = GetGPT(req); - let songIDs = undefined; + let songIDs; if (IsString(req.query.search)) { const songs = await SearchGameSongs(game, req.query.search, 100); + songIDs = songs.map((e) => e.id); } @@ -57,20 +58,20 @@ 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_TachiAPIAuth].userID) { + if (!req[SYMBOL_TACHI_API_AUTH].userID) { charts = charts.filter( - (e) => (e as ChartDocument<"iidx:SP" | "iidx:DP">).data["2dxtraSet"] === null + (e) => (e as ChartDocument<"iidx:DP" | "iidx:SP">).data["2dxtraSet"] === null ); } else { const iidxSettings = (await db["game-settings"].findOne({ - userID: req[SYMBOL_TachiAPIAuth].userID!, + userID: req[SYMBOL_TACHI_API_AUTH].userID!, game, playtype, - })) as UGPTSettings<"iidx:SP" | "iidx:DP"> | null; + })) as UGPTSettings<"iidx:DP" | "iidx:SP"> | null; if (!iidxSettings || !iidxSettings.preferences.gameSpecific.display2DXTra) { charts = charts.filter( - (e) => (e as ChartDocument<"iidx:SP" | "iidx:DP">).data["2dxtraSet"] === null + (e) => (e as ChartDocument<"iidx:DP" | "iidx:SP">).data["2dxtraSet"] === null ); } } diff --git a/server/src/server/router/api/v1/games/_game/_playtype/folders/middleware.ts b/server/src/server/router/api/v1/games/_game/_playtype/folders/middleware.ts index 6cba90bfe..e88a0a010 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/folders/middleware.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/folders/middleware.ts @@ -1,6 +1,6 @@ -import { RequestHandler } from "express"; import db from "external/mongo/db"; import { AssignToReqTachiData, GetGPT } from "utils/req-tachi-data"; +import type { RequestHandler } from "express"; export const GetFolderFromParam: RequestHandler = async (req, res, next) => { const { game, playtype } = GetGPT(req); @@ -16,5 +16,5 @@ export const GetFolderFromParam: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { folderDoc: folder }); - return next(); + next(); }; 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 a7e11e8ed..c580b1d20 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 @@ -1,11 +1,11 @@ +import { GetFolderFromParam } from "./middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { SearchCollection } from "lib/search/search"; import { GetFolderCharts } from "utils/folder"; import { IsString } from "utils/misc"; import { GetGPT } from "utils/req-tachi-data"; -import { GetFolderFromParam } from "./middleware"; const router: Router = Router({ mergeParams: true }); @@ -30,7 +30,7 @@ router.get("/", async (req, res) => { const folders = await SearchCollection( db.folders, req.query.search, - { game, playtype, inactive: !!req.query.inactive }, + { game, playtype, inactive: Boolean(req.query.inactive) }, 100 ); @@ -47,7 +47,7 @@ router.get("/", async (req, res) => { * @name GET /api/v1/games/:game/:playtype/folders/:folderID */ router.get("/:folderID", GetFolderFromParam, async (req, res) => { - const folder = req[SYMBOL_TachiData]!.folderDoc!; + const folder = req[SYMBOL_TACHI_DATA]!.folderDoc!; const { songs, charts } = await GetFolderCharts(folder, {}, true); diff --git a/server/src/server/router/api/v1/games/_game/_playtype/middleware.ts b/server/src/server/router/api/v1/games/_game/_playtype/middleware.ts index 422e1d4ed..26143edd5 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/middleware.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/middleware.ts @@ -1,10 +1,11 @@ -import { Game, GetGameConfig, Playtype, Playtypes } from "tachi-common"; -import { RequestHandler } from "express"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; +import { Game, GetGameConfig, Playtypes } from "tachi-common"; import { AssignToReqTachiData } from "utils/req-tachi-data"; +import type { RequestHandler } from "express"; +import type { Playtype } from "tachi-common"; export const ValidatePlaytypeFromParam: RequestHandler = (req, res, next) => { - const game = req[SYMBOL_TachiData]!.game!; + const game = req[SYMBOL_TACHI_DATA]!.game!; const gameConfig = GetGameConfig(game); @@ -17,5 +18,5 @@ export const ValidatePlaytypeFromParam: RequestHandler = (req, res, next) => { AssignToReqTachiData(req, { playtype: req.params.playtype as Playtype }); - return next(); + next(); }; 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 33ee83f5c..bb50f5b61 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 @@ -1,19 +1,17 @@ +import chartsRouter from "./charts/router"; +import foldersRouter from "./folders/router"; +import { ValidatePlaytypeFromParam } from "./middleware"; +import scoresRouter from "./scores/router"; +import songIDRouter from "./songs/_songID/router"; +import tablesRouter from "./tables/router"; +import targetsRouter from "./targets/router"; import { Router } from "express"; import db from "external/mongo/db"; import { ONE_HOUR } from "lib/constants/time"; -import { FindOptions } from "monk"; import NodeCache from "node-cache"; import p from "prudence"; import prValidate from "server/middleware/prudence-validate"; -import { - FormatGame, - Game, - GetGamePTConfig, - integer, - Playtype, - Playtypes, - UserGameStats, -} from "tachi-common"; +import { FormatGame, GetGamePTConfig, Playtypes } from "tachi-common"; import { GetRelevantSongsAndCharts } from "utils/db"; import { IsString } from "utils/misc"; import { GetClassDistribution } from "utils/queries/stats"; @@ -24,13 +22,8 @@ import { ParseStrPositiveNonZeroInt, } from "utils/string-checks"; import { GetUsersWithIDs } from "utils/user"; -import chartsRouter from "./charts/router"; -import foldersRouter from "./folders/router"; -import { ValidatePlaytypeFromParam } from "./middleware"; -import scoresRouter from "./scores/router"; -import songIDRouter from "./songs/_songID/router"; -import tablesRouter from "./tables/router"; -import targetsRouter from "./targets/router"; +import type { FindOptions } from "monk"; +import type { Game, integer, Playtype, UserGameStats } from "tachi-common"; const router: Router = Router({ mergeParams: true }); @@ -110,6 +103,7 @@ router.get("/leaderboard", async (req, res) => { } let alg = gptConfig.defaultProfileRatingAlg; + if (IsString(req.query.alg)) { const temp = CheckStrProfileAlg(game, playtype, req.query.alg); @@ -174,6 +168,7 @@ router.get("/score-leaderboard", async (req, res) => { } let alg = gptConfig.defaultScoreRatingAlg; + if (IsString(req.query.alg)) { const temp = CheckStrScoreAlg(game, playtype, req.query.alg); @@ -237,6 +232,7 @@ router.get( const stat = req.query.class as string; const supportedClasses = Object.keys(gptConfig.classHumanisedFormat); + if (!supportedClasses.includes(stat)) { return res.status(400).json({ success: false, diff --git a/server/src/server/router/api/v1/games/_game/_playtype/songs/_songID/middleware.ts b/server/src/server/router/api/v1/games/_game/_playtype/songs/_songID/middleware.ts index e27f9b4ed..18db0bf07 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/songs/_songID/middleware.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/songs/_songID/middleware.ts @@ -1,8 +1,8 @@ -import { RequestHandler } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { AssignToReqTachiData } from "utils/req-tachi-data"; import { ParseStrPositiveInt } from "utils/string-checks"; +import type { RequestHandler } from "express"; export const ValidateAndGetSong: RequestHandler = async (req, res, next) => { const songID = ParseStrPositiveInt(req.params.songID); @@ -14,7 +14,7 @@ export const ValidateAndGetSong: RequestHandler = async (req, res, next) => { }); } - const game = req[SYMBOL_TachiData]!.game!; + const game = req[SYMBOL_TACHI_DATA]!.game!; const song = await db.songs[game].findOne({ id: songID }); @@ -27,5 +27,5 @@ export const ValidateAndGetSong: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { songDoc: song }); - return next(); + next(); }; diff --git a/server/src/server/router/api/v1/games/_game/_playtype/songs/_songID/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/songs/_songID/router.ts index d0a2d3144..b761140f2 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/songs/_songID/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/songs/_songID/router.ts @@ -1,8 +1,8 @@ +import { ValidateAndGetSong } from "./middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { GetGPT } from "utils/req-tachi-data"; -import { ValidateAndGetSong } from "./middleware"; const router: Router = Router({ mergeParams: true }); @@ -14,7 +14,7 @@ router.use(ValidateAndGetSong); * @name GET /api/v1/games/:game/:playtype/songs/:songID */ router.get("/", async (req, res) => { - const song = req[SYMBOL_TachiData]!.songDoc!; + const song = req[SYMBOL_TACHI_DATA]!.songDoc!; const { game, playtype } = GetGPT(req); const charts = await db.charts[game].find({ diff --git a/server/src/server/router/api/v1/games/_game/_playtype/tables/middleware.ts b/server/src/server/router/api/v1/games/_game/_playtype/tables/middleware.ts index 127806c5a..86a2d6a78 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/tables/middleware.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/tables/middleware.ts @@ -1,11 +1,11 @@ -import { RequestHandler } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { AssignToReqTachiData } from "utils/req-tachi-data"; +import type { RequestHandler } from "express"; export const GetTableFromParam: RequestHandler = async (req, res, next) => { - const game = req[SYMBOL_TachiData]!.game; - const playtype = req[SYMBOL_TachiData]!.playtype; + const game = req[SYMBOL_TACHI_DATA]!.game; + const playtype = req[SYMBOL_TACHI_DATA]!.playtype; const table = await db.tables.findOne({ tableID: req.params.tableID, game, playtype }); @@ -18,5 +18,5 @@ export const GetTableFromParam: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { tableDoc: table }); - return next(); + next(); }; 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 3e420b134..55486348e 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 @@ -1,11 +1,11 @@ +import { GetTableFromParam } from "./middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; -import { FilterQuery } from "mongodb"; -import { TableDocument } from "tachi-common"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { GetFoldersFromTable } from "utils/folder"; import { GetGPT } from "utils/req-tachi-data"; -import { GetTableFromParam } from "./middleware"; +import type { FilterQuery } from "mongodb"; +import type { TableDocument } from "tachi-common"; const router: Router = Router({ mergeParams: true }); @@ -40,7 +40,7 @@ router.get("/", async (req, res) => { * @name GET /api/v1/games/:game/:playtype/tables/:tableID */ router.get("/:tableID", GetTableFromParam, async (req, res) => { - const table = req[SYMBOL_TachiData]!.tableDoc!; + const table = req[SYMBOL_TACHI_DATA]!.tableDoc!; const folders = await GetFoldersFromTable(table); diff --git a/server/src/server/router/api/v1/games/_game/_playtype/targets/goals/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/targets/goals/router.ts index e828ac353..21c2e01be 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/targets/goals/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/targets/goals/router.ts @@ -1,17 +1,15 @@ -import { RequestHandler, Router } from "express"; +import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; -import { - EvaluatedGoalReturn, - EvaluateGoalForUser, - GetMilestonesThatContainGoal, -} from "lib/targets/goals"; +import { EvaluateGoalForUser, GetMilestonesThatContainGoal } from "lib/targets/goals"; import prValidate from "server/middleware/prudence-validate"; import { FormatGame } from "tachi-common"; import { GetMostSubscribedGoals } from "utils/db"; import { AssignToReqTachiData, GetGPT } from "utils/req-tachi-data"; import { GetUsersWithIDs, ResolveUser } from "utils/user"; +import type { RequestHandler } from "express"; +import type { EvaluatedGoalReturn } from "lib/targets/goals"; const logger = CreateLogCtx(__filename); @@ -53,7 +51,7 @@ const ResolveGoalID: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { goalDoc: goal }); - return next(); + next(); }; /** @@ -62,7 +60,7 @@ const ResolveGoalID: RequestHandler = async (req, res, next) => { * @name GET /api/v1/games/:game/:playtype/targets/goals/:goalID */ router.get("/:goalID", ResolveGoalID, async (req, res) => { - const goal = req[SYMBOL_TachiData]!.goalDoc!; + const goal = req[SYMBOL_TACHI_DATA]!.goalDoc!; const goalSubs = await db["goal-subs"].find({ goalID: goal.goalID, @@ -127,7 +125,7 @@ router.get( }); } - const goal = req[SYMBOL_TachiData]!.goalDoc!; + const goal = req[SYMBOL_TACHI_DATA]!.goalDoc!; const goalID = goal.goalID; const goalSub = await db["goal-subs"].findOne({ @@ -136,7 +134,9 @@ router.get( }); let goalResults: EvaluatedGoalReturn; + // shortcut evaluation by using the user goal + if (goalSub) { goalResults = { achieved: goalSub.achieved, diff --git a/server/src/server/router/api/v1/games/_game/_playtype/targets/milestone-sets/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/targets/milestone-sets/router.ts index cbf741e41..0a651914d 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/targets/milestone-sets/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/targets/milestone-sets/router.ts @@ -1,10 +1,11 @@ -import { RequestHandler, Router } from "express"; +import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { SearchCollection } from "lib/search/search"; import { GetChildMilestones } from "utils/db"; import { IsString } from "utils/misc"; import { AssignToReqTachiData, GetGPT } from "utils/req-tachi-data"; +import type { RequestHandler } from "express"; const router: Router = Router({ mergeParams: true }); @@ -27,7 +28,7 @@ const ResolveMilestoneSetID: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { milestoneSetDoc: set }); - return next(); + next(); }; /** @@ -67,7 +68,7 @@ router.get("/", async (req, res) => { * @name GET /api/v1/games/:game/:playtype/targets/milestone-sets/:setID */ router.get("/:setID", ResolveMilestoneSetID, async (req, res) => { - const milestoneSet = req[SYMBOL_TachiData]!.milestoneSetDoc!; + const milestoneSet = req[SYMBOL_TACHI_DATA]!.milestoneSetDoc!; const milestones = await GetChildMilestones(milestoneSet); diff --git a/server/src/server/router/api/v1/games/_game/_playtype/targets/milestones/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/targets/milestones/router.ts index 3f4fce551..cf433b6b0 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/targets/milestones/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/targets/milestones/router.ts @@ -1,6 +1,6 @@ -import { RequestHandler, Router } from "express"; +import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { SearchCollection } from "lib/search/search"; import { EvaluateMilestoneProgress, GetGoalsInMilestone } from "lib/targets/milestones"; import prValidate from "server/middleware/prudence-validate"; @@ -9,6 +9,7 @@ import { GetMostSubscribedMilestones } from "utils/db"; import { IsString } from "utils/misc"; import { AssignToReqTachiData, GetGPT } from "utils/req-tachi-data"; import { GetUsersWithIDs, ResolveUser } from "utils/user"; +import type { RequestHandler } from "express"; const router: Router = Router({ mergeParams: true }); @@ -31,7 +32,7 @@ const ResolveMilestoneID: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { milestoneDoc: milestone }); - return next(); + next(); }; /** @@ -88,7 +89,7 @@ router.get("/popular", async (req, res) => { * @name GET /api/v1/games/:game/:playtype/targets/milestones/:milestoneID */ router.get("/:milestoneID", ResolveMilestoneID, async (req, res) => { - const milestone = req[SYMBOL_TachiData]!.milestoneDoc!; + const milestone = req[SYMBOL_TACHI_DATA]!.milestoneDoc!; const milestoneSubs = await db["milestone-subs"].find({ milestoneID: milestone.milestoneID, @@ -156,7 +157,7 @@ router.get( }); } - const milestone = req[SYMBOL_TachiData]!.milestoneDoc!; + const milestone = req[SYMBOL_TACHI_DATA]!.milestoneDoc!; const milestoneProgress = await EvaluateMilestoneProgress(user.id, milestone); diff --git a/server/src/server/router/api/v1/games/_game/_playtype/targets/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/targets/router.ts index 0763f162d..b79b2490f 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/targets/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/targets/router.ts @@ -1,3 +1,6 @@ +import goalsRouter from "./goals/router"; +import milestoneSetsRouter from "./milestone-sets/router"; +import milestonesRouter from "./milestones/router"; import { Router } from "express"; import { FormatGame } from "tachi-common"; import { @@ -7,9 +10,6 @@ import { GetRecentlyInteractedMilestones, } from "utils/db"; import { GetGPT } from "utils/req-tachi-data"; -import goalsRouter from "./goals/router"; -import milestonesRouter from "./milestones/router"; -import milestoneSetsRouter from "./milestone-sets/router"; const router: Router = Router({ mergeParams: true }); diff --git a/server/src/server/router/api/v1/games/_game/middleware.ts b/server/src/server/router/api/v1/games/_game/middleware.ts index bc1f33244..b7aaa9db7 100644 --- a/server/src/server/router/api/v1/games/_game/middleware.ts +++ b/server/src/server/router/api/v1/games/_game/middleware.ts @@ -1,7 +1,7 @@ -import { RequestHandler } from "express"; import { TachiConfig } from "lib/setup/config"; import { IsValidGame } from "utils/misc"; import { AssignToReqTachiData } from "utils/req-tachi-data"; +import type { RequestHandler } from "express"; export const ValidateGameFromParam: RequestHandler = (req, res, next) => { if (!IsValidGame(req.params.game)) { @@ -15,5 +15,5 @@ export const ValidateGameFromParam: RequestHandler = (req, res, next) => { AssignToReqTachiData(req, { game: req.params.game }); - return next(); + next(); }; diff --git a/server/src/server/router/api/v1/games/_game/router.ts b/server/src/server/router/api/v1/games/_game/router.ts index d280a5ea0..8f6110912 100644 --- a/server/src/server/router/api/v1/games/_game/router.ts +++ b/server/src/server/router/api/v1/games/_game/router.ts @@ -1,8 +1,8 @@ -import { Router } from "express"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; -import { GetGameConfig } from "tachi-common"; -import { ValidateGameFromParam } from "./middleware"; import playtypeRouter from "./_playtype/router"; +import { ValidateGameFromParam } from "./middleware"; +import { Router } from "express"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; +import { GetGameConfig } from "tachi-common"; const router: Router = Router({ mergeParams: true }); @@ -14,7 +14,7 @@ router.use(ValidateGameFromParam); * @name GET /api/v1/games/:game */ router.get("/", (req, res) => { - const game = req[SYMBOL_TachiData]!.game!; + const game = req[SYMBOL_TACHI_DATA]!.game!; return res.status(200).json({ success: true, diff --git a/server/src/server/router/api/v1/games/router.ts b/server/src/server/router/api/v1/games/router.ts index b74f08aad..46cfdc403 100644 --- a/server/src/server/router/api/v1/games/router.ts +++ b/server/src/server/router/api/v1/games/router.ts @@ -1,7 +1,7 @@ +import gameRouter from "./_game/router"; import { Router } from "express"; import { TachiConfig } from "lib/setup/config"; import { GetGameConfig } from "tachi-common"; -import gameRouter from "./_game/router"; const router: Router = Router({ mergeParams: true }); diff --git a/server/src/server/router/api/v1/import/router.ts b/server/src/server/router/api/v1/import/router.ts index 695e00f28..af71b900b 100644 --- a/server/src/server/router/api/v1/import/router.ts +++ b/server/src/server/router/api/v1/import/router.ts @@ -1,21 +1,21 @@ import { Router } from "express"; import db from "external/mongo/db"; import { SIXTEEN_MEGABTYES } from "lib/constants/filesize"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper"; import { ReprocessOrphan } from "lib/score-import/framework/orphans/orphans"; import { MakeScoreImport } from "lib/score-import/framework/score-import"; -import { ScoreImportJobData } from "lib/score-import/worker/types"; import { ServerConfig, TachiConfig } from "lib/setup/config"; import Prudence from "prudence"; import { RequirePermissions } from "server/middleware/auth"; import { CreateMulterSingleUploadMiddleware } from "server/middleware/multer-upload"; import prValidate from "server/middleware/prudence-validate"; import { ScoreImportRateLimiter } from "server/middleware/rate-limiter"; -import { APIImportTypes, FileUploadImportTypes } from "tachi-common"; import { Random20Hex } from "utils/misc"; import { FormatUserDoc, GetUserWithIDGuaranteed } from "utils/user"; +import type { ScoreImportJobData } from "lib/score-import/worker/types"; +import type { APIImportTypes, FileUploadImportTypes } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -60,14 +60,14 @@ router.post( const importType = req.body.importType as FileUploadImportTypes; - const userIntent = !!req.header("X-User-Intent"); + const userIntent = Boolean(req.header("X-User-Intent")); if (ServerConfig.USE_EXTERNAL_SCORE_IMPORT_WORKER) { const importID = Random20Hex(); const job: ScoreImportJobData = { importID, - userID: req[SYMBOL_TachiAPIAuth].userID!, + userID: req[SYMBOL_TACHI_API_AUTH].userID!, userIntent, importType, parserArguments: [req.file, req.body], @@ -85,17 +85,17 @@ router.post( importID, }, }); - } else { - // Fire the score import and wait for it to finish! - const importResponse = await ExpressWrappedScoreImportMain( - req[SYMBOL_TachiAPIAuth].userID!, - userIntent, - importType, - [req.file, req.body] - ); - - return res.status(importResponse.statusCode).json(importResponse.body); } + + // Fire the score import and wait for it to finish! + const importResponse = await ExpressWrappedScoreImportMain( + req[SYMBOL_TACHI_API_AUTH].userID!, + userIntent, + importType, + [req.file, req.body] + ); + + return res.status(importResponse.statusCode).json(importResponse.body); } ); @@ -118,9 +118,9 @@ router.post( const importID = Random20Hex(); - const userID = req[SYMBOL_TachiAPIAuth].userID!; + const userID = req[SYMBOL_TACHI_API_AUTH].userID!; - const userIntent = !!req.header("X-User-Intent"); + const userIntent = Boolean(req.header("X-User-Intent")); if (ServerConfig.USE_EXTERNAL_SCORE_IMPORT_WORKER) { const job: ScoreImportJobData = { @@ -143,17 +143,17 @@ router.post( importID, }, }); - } else { - // Fire the score import and wait for it to finish! - const importResponse = await ExpressWrappedScoreImportMain( - userID, - userIntent, - importType, - [userID] - ); - - return res.status(importResponse.statusCode).json(importResponse.body); } + + // Fire the score import and wait for it to finish! + const importResponse = await ExpressWrappedScoreImportMain( + userID, + userIntent, + importType, + [userID] + ); + + return res.status(importResponse.statusCode).json(importResponse.body); } ); @@ -164,7 +164,7 @@ router.post( * @name POST /api/v1/import/orphans */ router.post("/orphans", RequirePermissions("submit_score"), async (req, res) => { - const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TachiAPIAuth].userID!); + const userDoc = await GetUserWithIDGuaranteed(req[SYMBOL_TACHI_API_AUTH].userID!); logger.info(`User ${FormatUserDoc(userDoc)} forced an orphan sync.`); diff --git a/server/src/server/router/api/v1/imports/middleware.ts b/server/src/server/router/api/v1/imports/middleware.ts index 8910a8c63..3de5e43ff 100644 --- a/server/src/server/router/api/v1/imports/middleware.ts +++ b/server/src/server/router/api/v1/imports/middleware.ts @@ -1,9 +1,9 @@ -import { RequestHandler } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData, SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA, SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { AssignToReqTachiData } from "utils/req-tachi-data"; import { IsRequesterAdmin } from "utils/user"; +import type { RequestHandler } from "express"; const logger = CreateLogCtx(__filename); @@ -19,12 +19,12 @@ export const GetImportFromParam: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { importDoc }); - return next(); + next(); }; export const RequireOwnershipOfImportOrAdmin: RequestHandler = async (req, res, next) => { - const importDoc = req[SYMBOL_TachiData]!.importDoc!; - const userID = req[SYMBOL_TachiAPIAuth].userID; + const importDoc = req[SYMBOL_TACHI_DATA]!.importDoc!; + const userID = req[SYMBOL_TACHI_API_AUTH].userID; if (userID === null) { return res.status(401).json({ @@ -34,9 +34,10 @@ export const RequireOwnershipOfImportOrAdmin: RequestHandler = async (req, res, } if (importDoc.userID !== userID) { - if (await IsRequesterAdmin(req[SYMBOL_TachiAPIAuth])) { + if (await IsRequesterAdmin(req[SYMBOL_TACHI_API_AUTH])) { logger.info(`Admin ${userID} interacted with someone elses import.`); - return next(); + next(); + return; } return res.status(403).json({ @@ -45,5 +46,5 @@ export const RequireOwnershipOfImportOrAdmin: RequestHandler = async (req, res, }); } - return next(); + next(); }; diff --git a/server/src/server/router/api/v1/imports/router.ts b/server/src/server/router/api/v1/imports/router.ts index 535b5fcc1..5f548a874 100644 --- a/server/src/server/router/api/v1/imports/router.ts +++ b/server/src/server/router/api/v1/imports/router.ts @@ -1,6 +1,7 @@ +import { GetImportFromParam, RequireOwnershipOfImportOrAdmin } from "./middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { JOB_RETRY_COUNT, SYMBOL_TachiData } from "lib/constants/tachi"; +import { JOB_RETRY_COUNT, SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { RevertImport } from "lib/imports/imports"; import CreateLogCtx from "lib/logger/logger"; import ScoreImportQueue, { ScoreImportQueueEvents } from "lib/score-import/worker/queue"; @@ -8,7 +9,6 @@ import { ServerConfig, TachiConfig } from "lib/setup/config"; import { RequirePermissions } from "server/middleware/auth"; import { GetRelevantSongsAndCharts } from "utils/db"; import { GetUserWithID } from "utils/user"; -import { GetImportFromParam, RequireOwnershipOfImportOrAdmin } from "./middleware"; const router: Router = Router({ mergeParams: true }); @@ -20,7 +20,7 @@ const logger = CreateLogCtx(__filename); * @name GET /api/v1/imports/:importID */ router.get("/:importID", GetImportFromParam, async (req, res) => { - const importDoc = req[SYMBOL_TachiData]!.importDoc!; + const importDoc = req[SYMBOL_TACHI_DATA]!.importDoc!; const scores = await db.scores.find({ scoreID: { $in: importDoc.scoreIDs }, @@ -73,7 +73,7 @@ router.post( RequireOwnershipOfImportOrAdmin, RequirePermissions("delete_score"), async (req, res) => { - const importDoc = req[SYMBOL_TachiData]!.importDoc!; + const importDoc = req[SYMBOL_TACHI_DATA]!.importDoc!; await RevertImport(importDoc); @@ -174,12 +174,12 @@ router.get("/:importID/poll-status", async (req, res) => { import: content.importDocument, }, }); - } else { - return res.status(content.statusCode).json({ - success: false, - description: content.description, - }); } + + return res.status(content.statusCode).json({ + success: false, + description: content.description, + }); } const progress = job.progress; diff --git a/server/src/server/router/api/v1/oauth/router.ts b/server/src/server/router/api/v1/oauth/router.ts index c1458cd15..2f7a81a98 100644 --- a/server/src/server/router/api/v1/oauth/router.ts +++ b/server/src/server/router/api/v1/oauth/router.ts @@ -74,6 +74,7 @@ router.post( userID: codeDoc.userID, token: Random20Hex(), identifier: `${client.name} Token`, + // converts ["a","b"] to {a: true, b: true}. permissions: Object.fromEntries(client.requestedPermissions.map((e) => [e, true])), fromAPIClient: client.clientID, diff --git a/server/src/server/router/api/v1/router.ts b/server/src/server/router/api/v1/router.ts index 968d9a4c0..9a0651522 100644 --- a/server/src/server/router/api/v1/router.ts +++ b/server/src/server/router/api/v1/router.ts @@ -1,5 +1,3 @@ -import { Router } from "express"; -import { NormalRateLimitMiddleware } from "server/middleware/rate-limiter"; import adminRouter from "./admin/router"; import authRouter from "./auth/router"; import clientsRouter from "./clients/router"; @@ -13,6 +11,8 @@ import searchRouter from "./search/router"; import sessionsRouter from "./sessions/router"; import statusRouter from "./status/router"; import usersRouter from "./users/router"; +import { Router } from "express"; +import { NormalRateLimitMiddleware } from "server/middleware/rate-limiter"; const router: Router = Router({ mergeParams: true }); diff --git a/server/src/server/router/api/v1/scores/_scoreID/middleware.ts b/server/src/server/router/api/v1/scores/_scoreID/middleware.ts index 8da816bf9..d12b797f1 100644 --- a/server/src/server/router/api/v1/scores/_scoreID/middleware.ts +++ b/server/src/server/router/api/v1/scores/_scoreID/middleware.ts @@ -1,9 +1,9 @@ -import { RequestHandler } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiAPIAuth, SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH, SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { AssignToReqTachiData } from "utils/req-tachi-data"; import { IsRequesterAdmin } from "utils/user"; +import type { RequestHandler } from "express"; const logger = CreateLogCtx(__filename); @@ -19,12 +19,12 @@ export const GetScoreFromParam: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { scoreDoc: score }); - return next(); + next(); }; export const RequireOwnershipOfScoreOrAdmin: RequestHandler = async (req, res, next) => { - const score = req[SYMBOL_TachiData]!.scoreDoc!; - const userID = req[SYMBOL_TachiAPIAuth].userID; + const score = req[SYMBOL_TACHI_DATA]!.scoreDoc!; + const userID = req[SYMBOL_TACHI_API_AUTH].userID; if (userID === null) { return res.status(401).json({ @@ -34,9 +34,10 @@ export const RequireOwnershipOfScoreOrAdmin: RequestHandler = async (req, res, n } if (score.userID !== userID) { - if (await IsRequesterAdmin(req[SYMBOL_TachiAPIAuth])) { + if (await IsRequesterAdmin(req[SYMBOL_TACHI_API_AUTH])) { logger.info(`Admin ${userID} interacted with someone elses .`); - return next(); + next(); + return; } return res.status(403).json({ @@ -45,5 +46,5 @@ export const RequireOwnershipOfScoreOrAdmin: RequestHandler = async (req, res, n }); } - return next(); + next(); }; 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 9f85ad2d0..0ea395453 100644 --- a/server/src/server/router/api/v1/scores/_scoreID/router.ts +++ b/server/src/server/router/api/v1/scores/_scoreID/router.ts @@ -1,13 +1,13 @@ +import { GetScoreFromParam, RequireOwnershipOfScoreOrAdmin } from "./middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; -import { DeleteScore } from "lib/score-mutation/delete-scores"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; +import { DeleteScore } from "lib/score-mutation/delete-scores"; import p from "prudence"; import { RequirePermissions } from "server/middleware/auth"; import prValidate from "server/middleware/prudence-validate"; import { GetUserWithID } from "utils/user"; -import { GetScoreFromParam, RequireOwnershipOfScoreOrAdmin } from "./middleware"; const router: Router = Router({ mergeParams: true }); @@ -23,7 +23,7 @@ router.use(GetScoreFromParam); * @name GET /api/v1/scores/:scoreID */ router.get("/", async (req, res) => { - const score = req[SYMBOL_TachiData]!.scoreDoc!; + const score = req[SYMBOL_TACHI_DATA]!.scoreDoc!; if (req.query.getRelated) { const [user, chart, song] = await Promise.all([ @@ -34,9 +34,9 @@ router.get("/", async (req, res) => { if (!user || !chart || !song) { logger.error( - `Score ${ - score.scoreID - } refers to non-existent data: [user,chart,song] [${!!user} ${!!chart} ${!!song}]` + `Score ${score.scoreID} refers to non-existent data: [user,chart,song] [${Boolean( + user + )} ${Boolean(chart)} ${Boolean(song)}]` ); return res.status(500).json({ @@ -87,7 +87,7 @@ router.patch( highlight: "*boolean", }), async (req, res) => { - const score = req[SYMBOL_TachiData]!.scoreDoc!; + const score = req[SYMBOL_TACHI_DATA]!.scoreDoc!; const modifyOption: ModifiableScoreProps = {}; @@ -146,9 +146,9 @@ router.delete( RequireOwnershipOfScoreOrAdmin, RequirePermissions("delete_score"), async (req, res) => { - const score = req[SYMBOL_TachiData]!.scoreDoc!; + const score = req[SYMBOL_TACHI_DATA]!.scoreDoc!; - await DeleteScore(score, !!req.body.blacklist); + await DeleteScore(score, Boolean(req.body.blacklist)); return res.status(200).json({ success: true, diff --git a/server/src/server/router/api/v1/scores/router.ts b/server/src/server/router/api/v1/scores/router.ts index e9f5073f6..31798b608 100644 --- a/server/src/server/router/api/v1/scores/router.ts +++ b/server/src/server/router/api/v1/scores/router.ts @@ -1,5 +1,5 @@ -import { Router } from "express"; import scoreIDRouter from "./_scoreID/router"; +import { Router } from "express"; const router: Router = Router({ mergeParams: true }); diff --git a/server/src/server/router/api/v1/sessions/_sessionID/middleware.ts b/server/src/server/router/api/v1/sessions/_sessionID/middleware.ts index bf181367c..7c4321f92 100644 --- a/server/src/server/router/api/v1/sessions/_sessionID/middleware.ts +++ b/server/src/server/router/api/v1/sessions/_sessionID/middleware.ts @@ -1,8 +1,8 @@ -import { RequestHandler } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiAPIAuth, SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH, SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { AssignToReqTachiData } from "utils/req-tachi-data"; +import type { RequestHandler } from "express"; const logger = CreateLogCtx(__filename); @@ -20,12 +20,12 @@ export const GetSessionFromParam: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { sessionDoc: session }); - return next(); + next(); }; export const RequireOwnershipOfSession: RequestHandler = (req, res, next) => { - const userID = req[SYMBOL_TachiAPIAuth].userID; - const session = req[SYMBOL_TachiData]!.sessionDoc!; + const userID = req[SYMBOL_TACHI_API_AUTH].userID; + const session = req[SYMBOL_TACHI_DATA]!.sessionDoc!; if (userID !== session.userID) { return res.status(403).json({ @@ -34,11 +34,11 @@ export const RequireOwnershipOfSession: RequestHandler = (req, res, next) => { }); } - return next(); + next(); }; export const UpdateSessionViewcount: RequestHandler = async (req, res, next) => { - const session = req[SYMBOL_TachiData]!.sessionDoc!; + const session = req[SYMBOL_TACHI_DATA]!.sessionDoc!; // Maybe we could improve how this cache works using redis. Maybe. // we don't need to actually check timestamp - this collection expires documents every 24hours. @@ -48,7 +48,8 @@ export const UpdateSessionViewcount: RequestHandler = async (req, res, next) => }); if (hasViewedRecently) { - return next(); + next(); + return; } // This sometimes fails due to a race condition in the above statement. @@ -80,5 +81,5 @@ export const UpdateSessionViewcount: RequestHandler = async (req, res, next) => ); } - return next(); + next(); }; 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 728b73e77..7dafce468 100644 --- a/server/src/server/router/api/v1/sessions/_sessionID/router.ts +++ b/server/src/server/router/api/v1/sessions/_sessionID/router.ts @@ -1,14 +1,14 @@ -import { Router } from "express"; -import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; -import { RequirePermissions } from "server/middleware/auth"; -import prValidate from "server/middleware/prudence-validate"; -import p from "prudence"; import { GetSessionFromParam, RequireOwnershipOfSession, UpdateSessionViewcount, } 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"; import { GetUserWithID } from "utils/user"; const router: Router = Router({ mergeParams: true }); @@ -21,7 +21,7 @@ router.use(GetSessionFromParam); * @name GET /api/v1/sessions/:sessionID */ router.get("/", UpdateSessionViewcount, async (req, res) => { - const session = req[SYMBOL_TachiData]!.sessionDoc!; + const session = req[SYMBOL_TACHI_DATA]!.sessionDoc!; const scores = await db.scores.find({ scoreID: { $in: session.scoreInfo.map((e) => e.scoreID) }, @@ -82,7 +82,7 @@ router.patch( { allowExcessKeys: true } ), async (req, res) => { - const session = req[SYMBOL_TachiData]!.sessionDoc!; + const session = req[SYMBOL_TACHI_DATA]!.sessionDoc!; const updateExp: ModifiableSessionProps = {}; diff --git a/server/src/server/router/api/v1/sessions/router.ts b/server/src/server/router/api/v1/sessions/router.ts index 78da96d1a..c72c5707e 100644 --- a/server/src/server/router/api/v1/sessions/router.ts +++ b/server/src/server/router/api/v1/sessions/router.ts @@ -1,5 +1,5 @@ -import { Router } from "express"; import sessionIDRouter from "./_sessionID/router"; +import { Router } from "express"; const router: Router = Router({ mergeParams: true }); diff --git a/server/src/server/router/api/v1/status/router.ts b/server/src/server/router/api/v1/status/router.ts index 970bc0120..e5d8afc0f 100644 --- a/server/src/server/router/api/v1/status/router.ts +++ b/server/src/server/router/api/v1/status/router.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import { VERSION_PRETTY } from "lib/constants/version"; const router: Router = Router({ mergeParams: true }); @@ -13,6 +13,7 @@ const startTime = Date.now(); */ router.get("/", (req, res) => { let echo; + if (req.query.echo && typeof req.query.echo === "string") { echo = req.query.echo; } @@ -24,9 +25,10 @@ router.get("/", (req, res) => { serverTime: Date.now(), startTime, version: VERSION_PRETTY, - whoami: req[SYMBOL_TachiAPIAuth].userID, + whoami: req[SYMBOL_TACHI_API_AUTH].userID, + // converts {foo: true, bar: false, baz: true} into [foo, baz] - permissions: Object.entries(req[SYMBOL_TachiAPIAuth].permissions) + permissions: Object.entries(req[SYMBOL_TACHI_API_AUTH].permissions) .filter((e) => e[1]) .map((e) => e[0]), echo, @@ -42,6 +44,7 @@ router.get("/", (req, res) => { */ router.post("/", (req, res) => { let echo; + if (req.body.echo && typeof req.body.echo === "string") { echo = req.body.echo; } @@ -53,9 +56,10 @@ router.post("/", (req, res) => { serverTime: Date.now(), startTime, version: VERSION_PRETTY, - whoami: req[SYMBOL_TachiAPIAuth].userID, + whoami: req[SYMBOL_TACHI_API_AUTH].userID, + // converts {foo: true, bar: false, baz: true} into [foo, baz] - permissions: Object.entries(req[SYMBOL_TachiAPIAuth].permissions) + permissions: Object.entries(req[SYMBOL_TACHI_API_AUTH].permissions) .filter((e) => e[1]) .map((e) => e[0]), echo, diff --git a/server/src/server/router/api/v1/users/_userID/api-tokens/router.ts b/server/src/server/router/api/v1/users/_userID/api-tokens/router.ts index 3e0138421..7e44cbb47 100644 --- a/server/src/server/router/api/v1/users/_userID/api-tokens/router.ts +++ b/server/src/server/router/api/v1/users/_userID/api-tokens/router.ts @@ -1,13 +1,14 @@ +import { RequireSelfRequestFromUser } from "../middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import p from "prudence"; import prValidate from "server/middleware/prudence-validate"; -import { ALL_PERMISSIONS, APIPermissions, APITokenDocument } from "tachi-common"; +import { ALL_PERMISSIONS } from "tachi-common"; import { Random20Hex } from "utils/misc"; import { FormatUserDoc } from "utils/user"; -import { RequireSelfRequestFromUser } from "../middleware"; +import type { APIPermissions, APITokenDocument } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -22,7 +23,7 @@ router.use(RequireSelfRequestFromUser); * @name GET /api/v1/users/:userID/api-tokens */ router.get("/", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; const keys = await db["api-tokens"].find({ userID: user.id, @@ -60,9 +61,9 @@ router.post( }); } - let permissions: APIPermissions[]; + let permissions: Array; - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; let identifier: string; let fromAPIClient = null; @@ -148,7 +149,7 @@ router.post( * @name DELETE /api/v1/users/:userID/api-token/:token */ router.delete("/:token", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; logger.info( `received request from ${FormatUserDoc(user)} to delete token ${req.params.token}.` 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 4257ebd40..d0339a71d 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 @@ -1,15 +1,15 @@ +import { RequireAuthedAsUser } from "../middleware"; import { Router } from "express"; 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_TachiData } from "lib/constants/tachi"; +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"; import { HashSHA256 } from "utils/crypto"; import { FormatUserDoc } from "utils/user"; -import { RequireAuthedAsUser } from "../middleware"; // note: this is just the ../pfp/router.ts code copied and altered. @@ -30,7 +30,7 @@ router.put( RequirePermissions("customise_profile"), CreateMulterSingleUploadMiddleware("banner", ONE_MEGABYTE, logger), async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; if (!user.customBannerLocation) { logger.verbose(`User ${FormatUserDoc(user)} set a custom profile banner.`); @@ -88,15 +88,16 @@ router.put( * @name GET /api/v1/users/:userID/banner */ router.get("/", (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; if (!user.customBannerLocation) { res.setHeader("Content-Type", "image/png"); - return CDNRedirect(res, "/users/default/banner"); + CDNRedirect(res, "/users/default/banner"); + return; } // express sniffs whether this is a png or jpg **and** browsers dont care either. - return CDNRedirect(res, GetProfileBannerURL(user.id, user.customBannerLocation)); + CDNRedirect(res, GetProfileBannerURL(user.id, user.customBannerLocation)); }); /** @@ -109,7 +110,7 @@ router.delete( RequireAuthedAsUser, RequirePermissions("customise_profile"), async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; if (!user.customBannerLocation) { return res.status(404).json({ 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 7631870e5..74c6e5683 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 @@ -1,13 +1,14 @@ +import { GetFolderFromParam } from "../../../../../../../games/_game/_playtype/folders/middleware"; +import { RequireSelfRequestFromUser } from "../../../../../middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; -import { FilterQuery } from "mongodb"; -import { GetGamePTConfig, ScoreDocument } from "tachi-common"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; +import { GetGamePTConfig } from "tachi-common"; import { GetFolderCharts, GetGradeLampDistributionForFolder, GetPBsOnFolder } from "utils/folder"; import { GetUGPT } from "utils/req-tachi-data"; import { ParseStrPositiveInt } from "utils/string-checks"; -import { GetFolderFromParam } from "../../../../../../../games/_game/_playtype/folders/middleware"; -import { RequireSelfRequestFromUser } from "../../../../../middleware"; +import type { FilterQuery } from "mongodb"; +import type { ScoreDocument } from "tachi-common"; const router: Router = Router({ mergeParams: true }); @@ -21,7 +22,7 @@ router.use(GetFolderFromParam); router.get("/", async (req, res) => { const { user } = GetUGPT(req); - const folder = req[SYMBOL_TachiData]!.folderDoc!; + const folder = req[SYMBOL_TACHI_DATA]!.folderDoc!; const { songs, charts, pbs } = await GetPBsOnFolder(user.id, folder); @@ -45,7 +46,7 @@ router.get("/", async (req, res) => { router.get("/stats", async (req, res) => { const { user } = GetUGPT(req); - const folder = req[SYMBOL_TachiData]!.folderDoc!; + const folder = req[SYMBOL_TACHI_DATA]!.folderDoc!; const stats = await GetGradeLampDistributionForFolder(user.id, folder); @@ -69,7 +70,7 @@ router.get("/stats", async (req, res) => { router.post("/viewed", RequireSelfRequestFromUser, async (req, res) => { const { user } = GetUGPT(req); - const folder = req[SYMBOL_TachiData]!.folderDoc!; + const folder = req[SYMBOL_TACHI_DATA]!.folderDoc!; await db["recent-folder-views"].update( { @@ -107,7 +108,7 @@ router.post("/viewed", RequireSelfRequestFromUser, async (req, res) => { router.get("/timeline", async (req, res) => { const { user, game, playtype } = GetUGPT(req); - const folder = req[SYMBOL_TachiData]!.folderDoc!; + const folder = req[SYMBOL_TACHI_DATA]!.folderDoc!; const gptConfig = GetGamePTConfig(game, playtype); const intIndex = ParseStrPositiveInt(req.query.criteriaValue); @@ -182,7 +183,7 @@ router.get("/timeline", async (req, res) => { ]) .then((r) => // eslint-disable-next-line @typescript-eslint/no-explicit-any - (r.map((e: any) => e.doc) as ScoreDocument[]).sort( + (r.map((e: any) => e.doc) as Array).sort( (a, b) => (a.timeAchieved ?? 0) - (b.timeAchieved ?? 0) ) ); 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 934037454..f7ff672f7 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 @@ -1,10 +1,10 @@ +import folderIDRouter from "./_folderID/router"; import { Router } from "express"; import db from "external/mongo/db"; import { SearchCollection } from "lib/search/search"; import { GetGradeLampDistributionForFolders, GetRecentlyViewedFolders } from "utils/folder"; import { IsString } from "utils/misc"; import { GetUGPT } from "utils/req-tachi-data"; -import folderIDRouter from "./_folderID/router"; const router: Router = Router({ mergeParams: true }); @@ -33,7 +33,7 @@ router.get("/", async (req, res) => { const folders = await SearchCollection( db.folders, req.query.search, - { game, playtype, inactive: !!req.query.inactive }, + { game, playtype, inactive: Boolean(req.query.inactive) }, 20 ); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/middleware.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/middleware.ts index 07b1f5e07..79bf4cfc4 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/middleware.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/middleware.ts @@ -1,11 +1,11 @@ -import { RequestHandler } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { IsValidGame, IsValidPlaytype } from "utils/misc"; import { AssignToReqTachiData } from "utils/req-tachi-data"; +import type { RequestHandler } from "express"; export const CheckUserPlayedGamePlaytype: RequestHandler = async (req, res, next) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; if (!IsValidGame(req.params.game)) { return res.status(400).json({ @@ -40,5 +40,5 @@ export const CheckUserPlayedGamePlaytype: RequestHandler = async (req, res, next playtype: req.params.playtype, }); - return next(); + next(); }; diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.ts index 5e355c97d..0e49078f8 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.ts @@ -1,12 +1,13 @@ +import { RequireAuthedAsUser } from "../../../../middleware"; import { Router } from "express"; import { SetRivalsFailReasons } from "lib/constants/err-codes"; import { GetChallengerUsers, GetRivalUsers, SetRivals } from "lib/rivals/rivals"; import p from "prudence"; import { RequirePermissions } from "server/middleware/auth"; import prValidate from "server/middleware/prudence-validate"; -import { FormatGame, integer } from "tachi-common"; +import { FormatGame } from "tachi-common"; import { GetUGPT } from "utils/req-tachi-data"; -import { RequireAuthedAsUser } from "../../../../middleware"; +import type { integer } from "tachi-common"; const router: Router = Router({ mergeParams: true }); @@ -42,7 +43,7 @@ router.put( rivalIDs: [p.isPositiveNonZeroInteger], }), async (req, res) => { - const rivalIDs: integer[] = req.body.rivalIDs; + const rivalIDs: Array = req.body.rivalIDs; const { user, game, playtype } = GetUGPT(req); const result = await SetRivals(user.id, game, playtype, rivalIDs); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts index a7056afd3..77f96c65a 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/router.ts @@ -1,16 +1,3 @@ -import { Router } from "express"; -import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; -import { GetGamePTConfig, integer, PBScoreDocument, UserGameStatsSnapshot } from "tachi-common"; -import { IsString } from "utils/misc"; -import { GetUGPT } from "utils/req-tachi-data"; -import { CheckStrProfileAlg } from "utils/string-checks"; -import { - GetAllRankings, - GetUGPTPlaycount, - GetUsersRankingAndOutOf, - GetUsersWithIDs, -} from "utils/user"; import foldersRouter from "./folders/router"; import { CheckUserPlayedGamePlaytype } from "./middleware"; import pbsRouter from "./pbs/router"; @@ -21,6 +8,20 @@ import settingsRouter from "./settings/router"; import showcaseRouter from "./showcase/router"; import tablesRouter from "./tables/router"; import targetsRouter from "./targets/router"; +import { Router } from "express"; +import db from "external/mongo/db"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; +import { GetGamePTConfig } from "tachi-common"; +import { IsString } from "utils/misc"; +import { GetUGPT } from "utils/req-tachi-data"; +import { CheckStrProfileAlg } from "utils/string-checks"; +import { + GetAllRankings, + GetUGPTPlaycount, + GetUsersRankingAndOutOf, + GetUsersWithIDs, +} from "utils/user"; +import type { integer, PBScoreDocument, UserGameStatsSnapshot } from "tachi-common"; const router: Router = Router({ mergeParams: true }); @@ -33,7 +34,7 @@ router.use(CheckUserPlayedGamePlaytype); router.get("/", async (req, res) => { const { game, playtype, user } = GetUGPT(req); - const stats = req[SYMBOL_TachiData]!.requestedUserGameStats!; + const stats = req[SYMBOL_TACHI_DATA]!.requestedUserGameStats!; const [totalScores, firstScore, mostRecentScore, rankingData] = await Promise.all([ db.scores.count({ @@ -90,7 +91,7 @@ router.get("/", async (req, res) => { router.get("/history", async (req, res) => { const { game, playtype, user } = GetUGPT(req); - const stats = req[SYMBOL_TachiData]!.requestedUserGameStats!; + const stats = req[SYMBOL_TACHI_DATA]!.requestedUserGameStats!; const snapshots = (await db["game-stats-snapshots"].find( { @@ -102,6 +103,7 @@ router.get("/history", async (req, res) => { sort: { timestamp: -1, }, + // avoid sending so much garbage. projection: { userID: 0, @@ -110,9 +112,9 @@ router.get("/history", async (req, res) => { }, limit: 30, } - )) as Omit[]; + )) as Array>; - const currentSnapshot: Omit = { + const currentSnapshot: Omit = { classes: stats.classes, ratings: stats.ratings, timestamp: Date.now(), // lazy, should probably be this midnight @@ -135,7 +137,7 @@ router.get("/history", async (req, res) => { router.get("/most-played", async (req, res) => { const { game, playtype, user } = GetUGPT(req); - const mostPlayed: { playcount: integer; songID: integer; _id: string }[] = + const mostPlayed: Array<{ playcount: integer; songID: integer; _id: string }> = await db.scores.aggregate([ { $match: { @@ -147,6 +149,7 @@ router.get("/most-played", async (req, res) => { { $group: { _id: "$chartID", + // micro opt songID: { $first: "$songID" }, playcount: { $sum: 1 }, @@ -178,7 +181,7 @@ router.get("/most-played", async (req, res) => { } // @ts-expect-error monkeypatching - const playcountPBs = pbs as (PBScoreDocument & { __playcount: integer })[]; + const playcountPBs = pbs as Array; // monkey patch __playcount on for (const pb of playcountPBs) { @@ -211,6 +214,7 @@ router.get("/leaderboard-adjacent", async (req, res) => { const gptConfig = GetGamePTConfig(game, playtype); let alg = gptConfig.defaultProfileRatingAlg; + if (IsString(req.query.alg)) { const temp = CheckStrProfileAlg(game, playtype, req.query.alg); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/sessions/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/sessions/router.ts index 509d81af1..712bcd3db 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/sessions/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/sessions/router.ts @@ -44,6 +44,7 @@ router.get("/best", async (req, res) => { const gptConfig = GetGamePTConfig(game, playtype); let alg = gptConfig.defaultSessionRatingAlg; + if (typeof req.query.alg === "string") { const userAlg = CheckStrSessionAlg(game, playtype, req.query.alg); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.ts index 4cd8914df..ac6c508a7 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/settings/router.ts @@ -1,3 +1,4 @@ +import { RequireAuthedAsUser } from "../../../../middleware"; import { Router } from "express"; import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; @@ -7,7 +8,6 @@ import { GetGamePTConfig } from "tachi-common"; import { FormatPrError, optNull } from "utils/prudence"; import { GetUGPT } from "utils/req-tachi-data"; import { FormatUserDoc } from "utils/user"; -import { RequireAuthedAsUser } from "../../../../middleware"; const logger = CreateLogCtx(__filename); @@ -52,6 +52,7 @@ router.patch( preferredSessionAlg: p.optional(p.nullable(p.isIn(gptConfig.sessionRatingAlgs))), preferredProfileAlg: p.optional(p.nullable(p.isIn(gptConfig.profileRatingAlgs))), defaultTable: "*?string", + // This is handled with game-specific schema validation below. gameSpecific: optNull(gameSpecificSchema), scoreBucket: optNull(p.isIn("grade", "lamp")), diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/showcase/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/showcase/router.ts index 466fb6dcb..85181deba 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/showcase/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/showcase/router.ts @@ -6,10 +6,12 @@ import { EvaluateUsersStatsShowcase } from "lib/showcase/get-stats"; import p from "prudence"; import { RequirePermissions } from "server/middleware/auth"; import { RequireAuthedAsUser } from "server/router/api/v1/users/_userID/middleware"; -import { GetGamePTConfig, ShowcaseStatDetails } from "tachi-common"; +import { GetGamePTConfig } from "tachi-common"; import { FormatPrError } from "utils/prudence"; import { GetUGPT } from "utils/req-tachi-data"; import { ResolveUser } from "utils/user"; +import type { ShowcaseStatDetails } from "tachi-common"; + const router: Router = Router({ mergeParams: true }); /** @@ -69,6 +71,7 @@ router.get("/custom", async (req, res) => { mode: p.is("folder"), property: p.isIn("grade", "lamp", "score", "percent"), folderID: "string", + // lazy regex for matching strings that look like numbers gte: p.regex(/^[0-9]*(.[0-9])?$/u), }, @@ -99,7 +102,7 @@ router.get("/custom", async (req, res) => { stat = { mode: "folder", - property: req.query.property as "grade" | "lamp" | "score" | "percent", + property: req.query.property as "grade" | "lamp" | "percent" | "score", folderID: folderIDs.length === 1 ? (req.query.folderID as string) : folderIDs, gte: Number(req.query.gte), }; @@ -133,7 +136,7 @@ router.get("/custom", async (req, res) => { stat = { mode: "chart", - property: req.query.property as "grade" | "lamp" | "score" | "percent" | "playcount", + property: req.query.property as "grade" | "lamp" | "percent" | "playcount" | "score", chartID: req.query.chartID as string, }; } else { @@ -180,6 +183,7 @@ router.put("/", RequireAuthedAsUser, RequirePermissions("customise_profile"), as for (const stat of req.body) { let err; + if (stat?.mode === "chart") { err = p(stat, { chartID: "string", @@ -199,6 +203,7 @@ router.put("/", RequireAuthedAsUser, RequirePermissions("customise_profile"), as }, mode: p.is("folder"), property: p.isIn("grade", "lamp", "score", "percent"), + // @ts-expect-error todo: investigate this weird prudence typeerror gte: (self, parent) => { if (typeof self !== "number") { @@ -206,9 +211,9 @@ router.put("/", RequireAuthedAsUser, RequirePermissions("customise_profile"), as } if (parent.property === "grade") { - return !!gptConfig.grades[self]; + return Boolean(gptConfig.grades[self]); } else if (parent.property === "lamp") { - return !!gptConfig.lamps[self]; + return Boolean(gptConfig.lamps[self]); } else if (parent.property === "score") { return p.isPositive(self); } else if (parent.property === "percent") { @@ -254,6 +259,7 @@ router.put("/", RequireAuthedAsUser, RequirePermissions("customise_profile"), as ) { return res.status(400).json({ success: false, + // this error message is kinda lazy. description: `Invalid folderID - must be a folder for this game and playtype.`, }); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/tables/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/tables/router.ts index c4f10feb3..c90920500 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/tables/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/tables/router.ts @@ -1,9 +1,10 @@ +import { GetTableFromParam } from "../../../../../../games/_game/_playtype/tables/middleware"; import { Router } from "express"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { GetFoldersFromTable, GetGradeLampDistributionForFolders } from "utils/folder"; import { GetUGPT } from "utils/req-tachi-data"; + // @todo maybe refactor where middleware is stored to avoid paths this ugly. -import { GetTableFromParam } from "../../../../../../games/_game/_playtype/tables/middleware"; const router: Router = Router({ mergeParams: true }); @@ -15,7 +16,7 @@ const router: Router = Router({ mergeParams: true }); router.get("/:tableID", GetTableFromParam, async (req, res) => { const { user } = GetUGPT(req); - const table = req[SYMBOL_TachiData]!.tableDoc!; + const table = req[SYMBOL_TACHI_DATA]!.tableDoc!; const folders = await GetFoldersFromTable(table); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts index 7410c943e..55035a096 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts @@ -1,7 +1,8 @@ -import { RequestHandler, Router } from "express"; +import { RequireAuthedAsUser } from "../../../../../middleware"; +import { Router } from "express"; import db from "external/mongo/db"; import { SubscribeFailReasons } from "lib/constants/err-codes"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { ServerConfig } from "lib/setup/config"; import { @@ -13,10 +14,10 @@ import { import p from "prudence"; import { RequirePermissions } from "server/middleware/auth"; import prValidate from "server/middleware/prudence-validate"; -import { GoalDocument, MilestoneDocument } from "tachi-common"; import { GetGoalForIDGuaranteed } from "utils/db"; import { AssignToReqTachiData, GetUGPT } from "utils/req-tachi-data"; -import { RequireAuthedAsUser } from "../../../../../middleware"; +import type { RequestHandler } from "express"; +import type { GoalDocument, MilestoneDocument } from "tachi-common"; const router: Router = Router({ mergeParams: true }); @@ -82,6 +83,7 @@ router.post( "scoreData.gradeIndex", "scoreData.score" ), + // we do proper validation on this later. value: p.gte(0), mode: p.isIn("single", "absolute", "proportion"), @@ -151,6 +153,7 @@ router.post( const data = req.body as GoalCreationBody; let goal; + try { goal = await ConstructGoal(data.charts, data.criteria, game, playtype); } catch (e) { @@ -210,7 +213,7 @@ const GetGoalSubscription: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { goalSubDoc: goalSub }); - return next(); + next(); }; /** @@ -221,9 +224,9 @@ const GetGoalSubscription: RequestHandler = async (req, res, next) => { router.get("/:goalID", GetGoalSubscription, async (req, res) => { const { user } = GetUGPT(req); - const goalSub = req[SYMBOL_TachiData]!.goalSubDoc!; + const goalSub = req[SYMBOL_TACHI_DATA]!.goalSubDoc!; - const milestones: MilestoneDocument[] = await GetMilestonesThatContainGoal(goalSub.goalID); + const milestones: Array = await GetMilestonesThatContainGoal(goalSub.goalID); const goal = await GetGoalForIDGuaranteed(goalSub.goalID); @@ -253,7 +256,7 @@ router.delete( const goalID = req.params.goalID; const { user, game, playtype } = GetUGPT(req); - const goalSub = req[SYMBOL_TachiData]!.goalSubDoc!; + const goalSub = req[SYMBOL_TACHI_DATA]!.goalSubDoc!; const parentMilestones = await GetBlockingParentMilestoneSubs(goalSub); diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/milestones/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/milestones/router.ts index cc3281956..6bdb2b619 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/milestones/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/milestones/router.ts @@ -1,7 +1,8 @@ -import { RequestHandler, Router } from "express"; +import { RequireAuthedAsUser } from "../../../../../middleware"; +import { Router } from "express"; import db from "external/mongo/db"; import { SubscribeFailReasons } from "lib/constants/err-codes"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { ServerConfig } from "lib/setup/config"; import { @@ -12,7 +13,7 @@ import { import { RequirePermissions } from "server/middleware/auth"; import { AssignToReqTachiData, GetGPT, GetUGPT } from "utils/req-tachi-data"; import { FormatUserDoc } from "utils/user"; -import { RequireAuthedAsUser } from "../../../../../middleware"; +import type { RequestHandler } from "express"; const logger = CreateLogCtx(__filename); @@ -72,7 +73,7 @@ const GetMilestoneSubscription: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { milestoneSubDoc: milestoneSub }); - return next(); + next(); }; const GetMilestone: RequestHandler = async (req, res, next) => { @@ -93,7 +94,7 @@ const GetMilestone: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { milestoneDoc: milestone }); - return next(); + next(); }; /** @@ -105,8 +106,8 @@ const GetMilestone: RequestHandler = async (req, res, next) => { router.get("/:milestoneID", GetMilestone, GetMilestoneSubscription, async (req, res) => { const { user } = GetUGPT(req); - const milestoneSub = req[SYMBOL_TachiData]!.milestoneSubDoc!; - const milestone = req[SYMBOL_TachiData]!.milestoneDoc!; + const milestoneSub = req[SYMBOL_TACHI_DATA]!.milestoneSubDoc!; + const milestone = req[SYMBOL_TACHI_DATA]!.milestoneDoc!; // Evaluate each goal for the user. This operation is much faster if the user is // subscribed to the milestone (they are), as we can just read their goalSub @@ -153,7 +154,7 @@ router.put( }); } - const milestone = req[SYMBOL_TachiData]!.milestoneDoc!; + const milestone = req[SYMBOL_TACHI_DATA]!.milestoneDoc!; const alreadySubscibed = await db["milestone-subs"].findOne({ userID: user.id, @@ -206,7 +207,7 @@ router.delete( RequirePermissions("manage_targets"), async (req, res) => { const { user } = GetUGPT(req); - const milestone = req[SYMBOL_TachiData]!.milestoneDoc!; + const milestone = req[SYMBOL_TACHI_DATA]!.milestoneDoc!; logger.info( `User ${FormatUserDoc(user)} is unsubscribing from milestone '${milestone.name}'.`, diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/router.ts index 2189b31ba..5fa1cd4ae 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/router.ts @@ -1,3 +1,5 @@ +import goalsRouter from "./goals/router"; +import milestonesRouter from "./milestones/router"; import { Router } from "express"; import { GetRecentlyAchievedGoals, @@ -6,8 +8,6 @@ import { GetRecentlyInteractedMilestones, } from "utils/db"; import { GetUGPT } from "utils/req-tachi-data"; -import goalsRouter from "./goals/router"; -import milestonesRouter from "./milestones/router"; const router: Router = Router({ mergeParams: true }); diff --git a/server/src/server/router/api/v1/users/_userID/imports/router.ts b/server/src/server/router/api/v1/users/_userID/imports/router.ts index 469261cb3..b8eb83ad1 100644 --- a/server/src/server/router/api/v1/users/_userID/imports/router.ts +++ b/server/src/server/router/api/v1/users/_userID/imports/router.ts @@ -1,6 +1,6 @@ import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import prValidate from "server/middleware/prudence-validate"; const router: Router = Router({ mergeParams: true }); @@ -17,7 +17,7 @@ const router: Router = Router({ mergeParams: true }); * @name GET /api/v1/users/:userID/imports */ router.get("/", prValidate({ timeFinished: "*string" }), async (req, res) => { - const userID = req[SYMBOL_TachiData]!.requestedUser!.id; + const userID = req[SYMBOL_TACHI_DATA]!.requestedUser!.id; let timeFinished = Infinity; @@ -60,7 +60,7 @@ router.get("/", prValidate({ timeFinished: "*string" }), async (req, res) => { * @name GET /api/v1/users/:userID/imports/with-user-intent */ router.get("/with-user-intent", async (req, res) => { - const userID = req[SYMBOL_TachiData]!.requestedUser!.id; + const userID = req[SYMBOL_TACHI_DATA]!.requestedUser!.id; const importsWithIntent = await db.imports.find({ userID, diff --git a/server/src/server/router/api/v1/users/_userID/integrations/arc/router.ts b/server/src/server/router/api/v1/users/_userID/integrations/arc/router.ts index e62f2bec8..ff31b3a5f 100644 --- a/server/src/server/router/api/v1/users/_userID/integrations/arc/router.ts +++ b/server/src/server/router/api/v1/users/_userID/integrations/arc/router.ts @@ -1,13 +1,13 @@ /* eslint-disable no-await-in-loop */ +import { RequireSelfRequestFromUser } from "../../middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import prValidate from "server/middleware/prudence-validate"; import { RequireKamaitachi } from "server/middleware/type-require"; import { GetArcAuth } from "utils/queries/auth"; import { FormatUserDoc } from "utils/user"; -import { RequireSelfRequestFromUser } from "../../middleware"; const router: Router = Router({ mergeParams: true }); @@ -21,7 +21,7 @@ router.use(RequireSelfRequestFromUser); * @name GET /api/v1/users/:userID/integrations/arc */ router.get("/", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; const [iidx, sdvx] = await Promise.all([ GetArcAuth(user.id, "api/arc-iidx"), @@ -43,7 +43,7 @@ router.get("/", async (req, res) => { * @name PATCH /api/v1/users/:userID/integrations/arc */ router.patch("/", prValidate({ iidx: "*?string", sdvx: "*?string" }), async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; if (Object.keys(req.body).length === 0) { return res.status(400).json({ diff --git a/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts index b371fc1ca..56061b367 100644 --- a/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts +++ b/server/src/server/router/api/v1/users/_userID/integrations/fervidex/router.ts @@ -1,11 +1,11 @@ +import { RequireSelfRequestFromUser } from "../../middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import prValidate from "server/middleware/prudence-validate"; import { RequireKamaitachi } from "server/middleware/type-require"; import { DeleteUndefinedProps } from "utils/misc"; import { optNull } from "utils/prudence"; -import { RequireSelfRequestFromUser } from "../../middleware"; const router: Router = Router({ mergeParams: true }); @@ -18,7 +18,7 @@ router.use(RequireSelfRequestFromUser); * @name GET /api/v1/users/:userID/integrations/fervidex/settings */ router.get("/settings", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; const settingsDoc = await db["fer-settings"].findOne({ userID: user.id, @@ -50,7 +50,7 @@ router.patch( }); } - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; const modifyDocument = req.body; diff --git a/server/src/server/router/api/v1/users/_userID/integrations/kai/_kaiType/middleware.ts b/server/src/server/router/api/v1/users/_userID/integrations/kai/_kaiType/middleware.ts index 34f1b9253..214c45081 100644 --- a/server/src/server/router/api/v1/users/_userID/integrations/kai/_kaiType/middleware.ts +++ b/server/src/server/router/api/v1/users/_userID/integrations/kai/_kaiType/middleware.ts @@ -1,4 +1,4 @@ -import { RequestHandler } from "express"; +import type { RequestHandler } from "express"; export const ValidateKaiType: RequestHandler = (req, res, next) => { if (!["min", "flo", "eag"].includes(req.params.kaiType.toLowerCase())) { @@ -8,5 +8,5 @@ export const ValidateKaiType: RequestHandler = (req, res, next) => { }); } - return next(); + next(); }; 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 fc0a5dac2..b83625242 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 @@ -1,6 +1,8 @@ +import { ValidateKaiType } from "./middleware"; +import { RequireSelfRequestFromUser } from "../../../middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { GetKaiTypeClientCredentials, @@ -9,12 +11,10 @@ import { import p from "prudence"; import prValidate from "server/middleware/prudence-validate"; import { RequireKamaitachi } from "server/middleware/type-require"; -import { URL } from "url"; import fetch from "utils/fetch"; import { GetKaiAuth } from "utils/queries/auth"; import { FormatUserDoc } from "utils/user"; -import { RequireSelfRequestFromUser } from "../../../middleware"; -import { ValidateKaiType } from "./middleware"; +import { URL } from "url"; const router: Router = Router({ mergeParams: true }); @@ -31,8 +31,8 @@ router.use(RequireKamaitachi, RequireSelfRequestFromUser, ValidateKaiType); */ // eslint-disable-next-line @typescript-eslint/no-explicit-any router.get("/", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; - const kaiType = req.params.kaiType.toUpperCase() as "FLO" | "EAG" | "MIN"; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; + const kaiType = req.params.kaiType.toUpperCase() as "EAG" | "FLO" | "MIN"; const authDoc = await GetKaiAuth(user.id, kaiType); @@ -40,7 +40,7 @@ router.get("/", async (req, res) => { success: true, description: authDoc ? `User is authenticated.` : `User is unauthenticated.`, body: { - authStatus: !!authDoc, + authStatus: Boolean(authDoc), }, }); }); @@ -68,8 +68,8 @@ router.post( "/oauth2callback", prValidate({ code: "string" }, {}, { allowExcessKeys: true }), async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; - const kaiType = req.params.kaiType.toUpperCase() as "FLO" | "EAG" | "MIN"; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; + const kaiType = req.params.kaiType.toUpperCase() as "EAG" | "FLO" | "MIN"; const baseUrl = KaiTypeToBaseURL(kaiType); @@ -121,6 +121,7 @@ router.post( } let json; + try { json = await getTokenRes.json(); } catch (err) { diff --git a/server/src/server/router/api/v1/users/_userID/integrations/router.ts b/server/src/server/router/api/v1/users/_userID/integrations/router.ts index bbcf46819..acdb984ed 100644 --- a/server/src/server/router/api/v1/users/_userID/integrations/router.ts +++ b/server/src/server/router/api/v1/users/_userID/integrations/router.ts @@ -1,7 +1,7 @@ -import { Router } from "express"; -import fervidexRouter from "./fervidex/router"; import arcRouter from "./arc/router"; +import fervidexRouter from "./fervidex/router"; import kaiKaiTypeRouter from "./kai/_kaiType/router"; +import { Router } from "express"; const router: Router = Router({ mergeParams: true }); 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 abb32ab02..97d80e839 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,12 +1,12 @@ +import { RequireSelfRequestFromUser } from "../middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { GetTotalAllowedInvites } from "lib/invites/invites"; import { RequireKamaitachi } from "server/middleware/type-require"; -import { InviteCodeDocument } from "tachi-common"; import { Random20Hex } from "utils/misc"; import { GetUsersWithIDs } from "utils/user"; -import { RequireSelfRequestFromUser } from "../middleware"; +import type { InviteCodeDocument } from "tachi-common"; const router: Router = Router({ mergeParams: true }); @@ -19,14 +19,14 @@ router.use(RequireSelfRequestFromUser); * @name GET /api/v1/users/:userID/invites */ router.get("/", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; const invites = await db.invites.find({ createdBy: user.id, }); const consumers = await GetUsersWithIDs( - invites.map((e) => e.consumedBy).filter((e) => e !== null) as number[] + invites.map((e) => e.consumedBy).filter((e) => e !== null) as Array ); return res.status(200).json({ @@ -43,7 +43,7 @@ router.get("/", async (req, res) => { * @name GET /api/v1/users/:userID/invites/limit */ router.get("/limit", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; const invites = await db.invites.count({ createdBy: user.id }); const limit = GetTotalAllowedInvites(user); @@ -66,7 +66,7 @@ const InviteLocks = new Set(); * @name POST /api/v1/users/:userID/invites/create */ router.post("/create", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; try { // race condition protection 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 081d07033..0b3170ff6 100644 --- a/server/src/server/router/api/v1/users/_userID/middleware.ts +++ b/server/src/server/router/api/v1/users/_userID/middleware.ts @@ -1,7 +1,7 @@ -import { RequestHandler } from "express"; -import { SYMBOL_TachiAPIAuth, SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH, SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { AssignToReqTachiData } from "utils/req-tachi-data"; import { ResolveUser } from "utils/user"; +import type { RequestHandler } from "express"; export const GetUserFromParam: RequestHandler = async (req, res, next) => { if (!req.params.userID) { @@ -14,7 +14,7 @@ export const GetUserFromParam: RequestHandler = async (req, res, next) => { let userID = req.params.userID; if (req.params.userID === "me") { - if (!req[SYMBOL_TachiAPIAuth].userID) { + if (!req[SYMBOL_TACHI_API_AUTH].userID) { return res.status(401).json({ success: false, description: "Cannot use 'me' userID with no authentication.", @@ -24,10 +24,11 @@ export const GetUserFromParam: RequestHandler = async (req, res, next) => { // fast assign using JWT if (req.session.tachi?.user) { AssignToReqTachiData(req, { requestedUser: req.session.tachi.user }); - return next(); + next(); + return; } - userID = req[SYMBOL_TachiAPIAuth].userID!.toString(); + userID = req[SYMBOL_TACHI_API_AUTH].userID!.toString(); } const user = await ResolveUser(userID); @@ -41,30 +42,30 @@ export const GetUserFromParam: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { requestedUser: user }); - return next(); + next(); }; /** * Require the user making this request to also be the user in the :userID param. */ export const RequireAuthedAsUser: RequestHandler = (req, res, next) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; - if (!req[SYMBOL_TachiAPIAuth].userID) { + if (!req[SYMBOL_TACHI_API_AUTH].userID) { return res.status(401).json({ success: false, description: `Authentication is required for this endpoint.`, }); } - if (req[SYMBOL_TachiAPIAuth].userID !== user.id) { + if (req[SYMBOL_TACHI_API_AUTH].userID !== user.id) { return res.status(403).json({ success: false, description: "You are not authorised as this user.", }); } - return next(); + next(); }; /** @@ -73,21 +74,21 @@ export const RequireAuthedAsUser: RequestHandler = (req, res, next) => { * alter/access, like integration information. */ export const RequireSelfRequestFromUser: RequestHandler = (req, res, next) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; - if (!req[SYMBOL_TachiAPIAuth].userID) { + if (!req[SYMBOL_TACHI_API_AUTH].userID) { return res.status(401).json({ success: false, description: `This endpoint requires session-level authentication.`, }); } - if (!req.session.tachi?.user.id || req[SYMBOL_TachiAPIAuth].userID !== user.id) { + if (!req.session.tachi?.user.id || req[SYMBOL_TACHI_API_AUTH].userID !== user.id) { return res.status(403).json({ success: false, description: `This request cannot be performed by an API key, and requires authentication.`, }); } - return next(); + next(); }; 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 45a017184..1b7537692 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,8 +1,8 @@ +import { RequireSelfRequestFromUser } from "../middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { ONE_SECOND } from "lib/constants/time"; -import { RequireSelfRequestFromUser } from "../middleware"; const router: Router = Router({ mergeParams: true }); @@ -16,7 +16,7 @@ router.use(RequireSelfRequestFromUser); * @name GET /api/v1/users/:userID/notifications */ router.get("/", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; const notifs = await db.notifications.find( { @@ -42,11 +42,12 @@ router.get("/", async (req, res) => { * @name POST /api/v1/users/:userID/notifications/mark-all-read */ router.post("/mark-all-read", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; const updateRes = await db.notifications.update( { sentTo: user.id, + // insanely rare edge case, but if someone submits an empty-my-inbox // request, and then gets a notif at the same time, they run the risk of // emptying something so immediately they don't actually ever see it. @@ -76,10 +77,11 @@ router.post("/mark-all-read", async (req, res) => { * @name POST /api/v1/users/:userID/notifications/delete-all */ router.post("/delete-all", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; const deleted = await db.notifications.remove({ sentTo: user.id, + // See mark-all-read for an explanation of this behaviour. sentAt: { $lt: Date.now() - ONE_SECOND * 2, 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 22b7ae445..be7618aff 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 @@ -1,15 +1,15 @@ +import { RequireAuthedAsUser } from "../middleware"; import { Router } from "express"; 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_TachiData } from "lib/constants/tachi"; +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"; import { HashSHA256 } from "utils/crypto"; import { FormatUserDoc } from "utils/user"; -import { RequireAuthedAsUser } from "../middleware"; const logger = CreateLogCtx(__filename); @@ -28,7 +28,7 @@ router.put( RequirePermissions("customise_profile"), CreateMulterSingleUploadMiddleware("pfp", ONE_MEGABYTE, logger), async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; if (!user.customPfpLocation) { logger.verbose(`User ${FormatUserDoc(user)} set a custom profile picture.`); @@ -66,6 +66,7 @@ router.put( if (req.session.tachi?.user) { req.session.tachi.user.customPfpLocation = contentHash; } + await db.users.update({ id: user.id }, { $set: { customPfpLocation: contentHash } }); return res.status(200).json({ @@ -85,16 +86,17 @@ router.put( * @name GET /api/v1/users/:userID/pfp */ router.get("/", (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; logger.debug("User Info for /:userID/pfp request is ", user); if (!user.customPfpLocation) { res.setHeader("Content-Type", "image/png"); - return CDNRedirect(res, "/users/default/pfp"); + CDNRedirect(res, "/users/default/pfp"); + return; } - return CDNRedirect(res, GetProfilePictureURL(user.id, user.customPfpLocation)); + CDNRedirect(res, GetProfilePictureURL(user.id, user.customPfpLocation)); }); /** @@ -107,7 +109,7 @@ router.delete( RequireAuthedAsUser, RequirePermissions("customise_profile"), async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; if (!user.customPfpLocation) { return res.status(404).json({ diff --git a/server/src/server/router/api/v1/users/_userID/router.ts b/server/src/server/router/api/v1/users/_userID/router.ts index 6721ab7b4..8641fadd5 100644 --- a/server/src/server/router/api/v1/users/_userID/router.ts +++ b/server/src/server/router/api/v1/users/_userID/router.ts @@ -1,11 +1,22 @@ +import apiTokensRouter from "./api-tokens/router"; +import bannerRouter from "./banner/router"; +import gamePTRouter from "./games/_game/_playtype/router"; +import importsRouter from "./imports/router"; +import integrationsRouter from "./integrations/router"; +import invitesRouter from "./invites/router"; +import { GetUserFromParam, RequireSelfRequestFromUser } from "./middleware"; +import notifsRouter from "./notifications/router"; +import pfpRouter from "./pfp/router"; +import settingsRouter from "./settings/router"; +import { HashPassword, PasswordCompare, ValidatePassword } from "../../auth/auth"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { ONE_MONTH } from "lib/constants/time"; import CreateLogCtx from "lib/logger/logger"; import p from "prudence"; import prValidate from "server/middleware/prudence-validate"; -import { ImportTypes, integer, UserGameStats } from "tachi-common"; +import { ImportTypes, integer } from "tachi-common"; import { DeleteUndefinedProps, StripUrl } from "utils/misc"; import { optNullFluffStrField } from "utils/prudence"; import { @@ -15,17 +26,7 @@ import { GetRecentSessions, } from "utils/queries/summary"; import { FormatUserDoc, GetAllRankings, GetUserWithID } from "utils/user"; -import { HashPassword, PasswordCompare, ValidatePassword } from "../../auth/auth"; -import apiTokensRouter from "./api-tokens/router"; -import bannerRouter from "./banner/router"; -import gamePTRouter from "./games/_game/_playtype/router"; -import integrationsRouter from "./integrations/router"; -import invitesRouter from "./invites/router"; -import { GetUserFromParam, RequireSelfRequestFromUser } from "./middleware"; -import pfpRouter from "./pfp/router"; -import settingsRouter from "./settings/router"; -import importsRouter from "./imports/router"; -import notifsRouter from "./notifications/router"; +import type { UserGameStats } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -38,7 +39,7 @@ router.use(GetUserFromParam); * @name GET /api/v1/users/:userID */ router.get("/", (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; return res.status(200).json({ success: true, @@ -86,7 +87,7 @@ router.patch( twitch: optNullFluffStrField, }), async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; if (Object.keys(req.body).length === 0) { return res.status(400).json({ @@ -102,17 +103,21 @@ router.patch( if (body.twitter) { body.twitter = StripUrl("twitter.com/", body.twitter); } + if (body.github) { body.github = StripUrl("github.com/", body.github); } + if (body.youtube) { // youtube has two user urls lol body.youtube = StripUrl("youtube.com/user/", body.youtube); body.youtube = StripUrl("youtube.com/channel/", body.youtube); } + if (body.twitch) { body.twitch = StripUrl("twitch.tv/", body.twitch); } + if (body.steam) { body.steam = StripUrl("steamcommunity.com/id/", body.steam); } @@ -127,8 +132,14 @@ router.patch( // :( const modifyObject: Partial< Record< - | `socialMedia.${"twitch" | "github" | "youtube" | "steam" | "twitter" | "discord"}` - | "status", + | "status" + | `socialMedia.${ + | "discord" + | "github" + | "steam" + | "twitch" + | "twitter" + | "youtube"}`, string | null > & { about: string } > = { @@ -193,15 +204,16 @@ router.patch( * @name GET /api/v1/users/:userID/game-stats */ router.get("/game-stats", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; // a user has played a game if and only if they have stats for it. - const stats: (UserGameStats & { __rankingData?: { outOf: number; ranking: number } })[] = + const stats: Array = await db["game-stats"].find({ userID: user.id }); await Promise.all( stats.map(async (s) => { const data = await GetAllRankings(s); + s.__rankingData = data; }) ); @@ -221,7 +233,7 @@ router.get("/game-stats", async (req, res) => { * @name GET /api/v1/users/:userID/recent-summary */ router.get("/recent-summary", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; const [ recentPlaycount, @@ -257,7 +269,7 @@ router.get("/recent-summary", async (req, res) => { * @name GET /api/v1/users/:userID/is-email-verified */ router.get("/is-email-verified", RequireSelfRequestFromUser, async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; const verifyInfo = await db["verify-email-codes"].findOne({ userID: user.id, @@ -302,6 +314,7 @@ router.post( logger.severe( `IP ${req.ip} got to /change-password without a user, but passed RequireSelfRequest?` ); + // this should be a 500, but lie to them. return res.status(403).json({ success: false, @@ -359,9 +372,9 @@ router.post( * Get the recent import types this user has used. */ router.get("/recent-imports", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; - const recentImports = (await db.imports.aggregate([ + const recentImports = await db.imports.aggregate([ { $match: { userID: user.id, @@ -375,7 +388,7 @@ router.get("/recent-imports", async (req, res) => { count: { $sum: 1 }, }, }, - ])) as { _id: ImportTypes; count: integer }[]; + ]); // rename _id to importType. const imports = recentImports.map((e) => ({ importType: e._id, count: e.count })); diff --git a/server/src/server/router/api/v1/users/_userID/settings/router.ts b/server/src/server/router/api/v1/users/_userID/settings/router.ts index dd60422bc..e98666ac4 100644 --- a/server/src/server/router/api/v1/users/_userID/settings/router.ts +++ b/server/src/server/router/api/v1/users/_userID/settings/router.ts @@ -1,11 +1,11 @@ +import { RequireSelfRequestFromUser } from "../middleware"; import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import prValidate from "server/middleware/prudence-validate"; import { DeleteUndefinedProps } from "utils/misc"; import { FormatUserDoc, GetSettingsForUser } from "utils/user"; -import { RequireSelfRequestFromUser } from "../middleware"; const logger = CreateLogCtx(__filename); const router: Router = Router({ mergeParams: true }); @@ -16,7 +16,7 @@ const router: Router = Router({ mergeParams: true }); * @name GET /api/v1/users/:userID/settings */ router.get("/", async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; const settings = await db["user-settings"].findOne({ userID: user.id, @@ -58,7 +58,7 @@ router.patch( deletableScores: "*boolean", }), async (req, res) => { - const user = req[SYMBOL_TachiData]!.requestedUser!; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser!; const preferences = { invisible: req.body.invisible, diff --git a/server/src/server/router/api/v1/users/router.ts b/server/src/server/router/api/v1/users/router.ts index f657e2a55..04c84c8f6 100644 --- a/server/src/server/router/api/v1/users/router.ts +++ b/server/src/server/router/api/v1/users/router.ts @@ -1,9 +1,9 @@ +import userIDRouter from "./_userID/router"; import { Router } from "express"; import db from "external/mongo/db"; import { SearchUsersRegExp } from "lib/search/search"; import { IsString } from "utils/misc"; import { GetOnlineCutoff } from "utils/user"; -import userIDRouter from "./_userID/router"; const router: Router = Router({ mergeParams: true }); @@ -17,7 +17,7 @@ const router: Router = Router({ mergeParams: true }); * @name GET /api/v1/users */ router.get("/", async (req, res) => { - const onlyOnline = !!req.query.online; + const onlyOnline = Boolean(req.query.online); let users; @@ -28,6 +28,7 @@ router.get("/", async (req, res) => { description: `Search parameter was invalid.`, }); } + users = await SearchUsersRegExp(req.query.search!, onlyOnline); } else { const query = onlyOnline ? { lastSeen: { $gt: GetOnlineCutoff() } } : {}; diff --git a/server/src/server/router/ir/barbatos/router.ts b/server/src/server/router/ir/barbatos/router.ts index 4e1f6ccec..142b5381a 100644 --- a/server/src/server/router/ir/barbatos/router.ts +++ b/server/src/server/router/ir/barbatos/router.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper"; import { RequirePermissions } from "server/middleware/auth"; @@ -13,7 +13,7 @@ router.use(RequirePermissions("submit_score")); */ router.post("/score/submit", async (req, res) => { const responseData = await ExpressWrappedScoreImportMain( - req[SYMBOL_TachiAPIAuth]!.userID!, + req[SYMBOL_TACHI_API_AUTH]!.userID!, false, "ir/barbatos", [req.body] diff --git a/server/src/server/router/ir/beatoraja/auth.ts b/server/src/server/router/ir/beatoraja/auth.ts index 06337c7b5..2081d895e 100644 --- a/server/src/server/router/ir/beatoraja/auth.ts +++ b/server/src/server/router/ir/beatoraja/auth.ts @@ -1,4 +1,4 @@ -import { RequestHandler } from "express"; +import type { RequestHandler } from "express"; export const ValidateIRClientVersion: RequestHandler = (req, res, next) => { const header = req.header("X-TachiIR-Version"); @@ -10,5 +10,5 @@ export const ValidateIRClientVersion: RequestHandler = (req, res, next) => { }); } - return next(); + next(); }; diff --git a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.ts b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.ts index 1a09dcda3..7923e5b8f 100644 --- a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.ts +++ b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/convert-scores.ts @@ -1,4 +1,4 @@ -import { integer, PBScoreDocument } from "tachi-common"; +import type { integer, PBScoreDocument } from "tachi-common"; const LAMP_TO_BEATORAJA = [0, 1, 3, 4, 5, 6, 7, 8] as const; @@ -10,13 +10,13 @@ const LAMP_TO_BEATORAJA = [0, 1, 3, 4, 5, 6, 7, 8] as const; // "S-RANDOM": 4, // } as const; -type BeatorajaJudgements = `${"e" | "l"}${"pg" | "gr" | "gd" | "bd" | "pr"}`; +type BeatorajaJudgements = `${"e" | "l"}${"bd" | "gd" | "gr" | "pg" | "pr"}`; type BeatorajaScoreJudgements = { [K in BeatorajaJudgements]: integer; }; -type BeatorajaPartialScoreFormat = { +interface BeatorajaPartialScoreFormat { sha256: string; player: string; playcount: integer; @@ -29,7 +29,7 @@ type BeatorajaPartialScoreFormat = { minbp: integer; notes: integer; maxcombo: integer | null; -}; +} export type BeatorajaIRScoreFormat = BeatorajaPartialScoreFormat & BeatorajaScoreJudgements; @@ -81,7 +81,7 @@ export function TachiScoreDataToBeatorajaFormat( "lpr", "ems", "lms", - ] as BeatorajaJudgements[]) { + ] as Array) { judgements[key] = scoreData.hitMeta[key] ?? 0; } @@ -97,6 +97,7 @@ export function TachiScoreDataToBeatorajaFormat( judgements.egr = pbScore.scoreData.score % 2; judgements.lpg = 0; judgements.lgr = 0; + // } return { ...beatorajaScore, ...judgements }; 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 95a449384..979d0ff59 100644 --- a/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.ts +++ b/server/src/server/router/ir/beatoraja/charts/_chartSHA256/router.ts @@ -1,9 +1,10 @@ -import { RequestHandler, Router } from "express"; -import db from "external/mongo/db"; -import { SYMBOL_TachiAPIAuth, SYMBOL_TachiData } from "lib/constants/tachi"; -import { ChartDocument, PBScoreDocument } from "tachi-common"; -import { AssignToReqTachiData } from "utils/req-tachi-data"; 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 { AssignToReqTachiData } from "utils/req-tachi-data"; +import type { RequestHandler } from "express"; +import type { ChartDocument, PBScoreDocument } from "tachi-common"; const router: Router = Router({ mergeParams: true }); @@ -31,7 +32,7 @@ const GetChartDocument: RequestHandler = async (req, res, next) => { AssignToReqTachiData(req, { beatorajaChartDoc: chart }); - return next(); + next(); }; router.use(GetChartDocument); @@ -42,12 +43,12 @@ router.use(GetChartDocument); * @name GET /ir/beatoraja/charts/:chartSHA256/scores */ router.get("/scores", async (req, res) => { - const chart = req[SYMBOL_TachiData]!.beatorajaChartDoc!; - const requestingUserID = req[SYMBOL_TachiAPIAuth].userID; + const chart = req[SYMBOL_TACHI_DATA]!.beatorajaChartDoc!; + const requestingUserID = req[SYMBOL_TACHI_API_AUTH].userID; const scores = (await db["personal-bests"].find({ chartID: chart.chartID, - })) as PBScoreDocument<"bms:7K" | "bms:14K" | "pms:Controller" | "pms:Keyboard">[]; + })) as Array>; const userDocs = await db.users.find( { @@ -61,6 +62,7 @@ router.get("/scores", async (req, res) => { } ); const userMap = new Map(); + for (const user of userDocs) { userMap.set(user.id, user); } diff --git a/server/src/server/router/ir/beatoraja/router.ts b/server/src/server/router/ir/beatoraja/router.ts index 56837661b..833ad47c9 100644 --- a/server/src/server/router/ir/beatoraja/router.ts +++ b/server/src/server/router/ir/beatoraja/router.ts @@ -1,15 +1,15 @@ -import { Router } from "express"; -import db from "external/mongo/db"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; -import CreateLogCtx from "lib/logger/logger"; -import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper"; -import { BeatorajaChart } from "lib/score-import/import-types/ir/beatoraja/types"; -import { ServerConfig } from "lib/setup/config"; -import { RequireNotGuest } from "server/middleware/auth"; -import { integer } from "tachi-common"; -import { UpdateClassIfGreater } from "utils/class"; import { ValidateIRClientVersion } from "./auth"; import chartsRouter from "./charts/_chartSHA256/router"; +import { Router } from "express"; +import db from "external/mongo/db"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; +import CreateLogCtx from "lib/logger/logger"; +import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper"; +import { ServerConfig } from "lib/setup/config"; +import { RequireNotGuest } from "server/middleware/auth"; +import { UpdateClassIfGreater } from "utils/class"; +import type { BeatorajaChart } from "lib/score-import/import-types/ir/beatoraja/types"; +import type { integer } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -23,7 +23,7 @@ router.use(ValidateIRClientVersion); * @name POST /ir/beatoraja/submit-score */ router.post("/submit-score", RequireNotGuest, async (req, res) => { - const userID = req[SYMBOL_TachiAPIAuth]!.userID!; + const userID = req[SYMBOL_TACHI_API_AUTH]!.userID!; const importRes = await ExpressWrappedScoreImportMain(userID, false, "ir/beatoraja", [ req.body, @@ -37,7 +37,7 @@ router.post("/submit-score", RequireNotGuest, async (req, res) => { const errMsg = importRes.body.body.errors[0].message; if (type === "KTDataNotFound") { - const orphanInfo: { userIDs: integer[] } | null = await db[ + const orphanInfo: { userIDs: Array } | null = await db[ "orphan-chart-queue" ].findOne( { @@ -218,7 +218,7 @@ router.post("/submit-course", RequireNotGuest, async (req, res) => { }); } - const userID = req[SYMBOL_TachiAPIAuth]!.userID!; + const userID = req[SYMBOL_TACHI_API_AUTH]!.userID!; const result = await UpdateClassIfGreater( userID, diff --git a/server/src/server/router/ir/direct-manual/router.ts b/server/src/server/router/ir/direct-manual/router.ts index 1625254cc..b70593df8 100644 --- a/server/src/server/router/ir/direct-manual/router.ts +++ b/server/src/server/router/ir/direct-manual/router.ts @@ -1,12 +1,12 @@ import { Router } from "express"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper"; import { MakeScoreImport } from "lib/score-import/framework/score-import"; -import { ScoreImportJobData } from "lib/score-import/worker/types"; import { ServerConfig } from "lib/setup/config"; import { RequirePermissions } from "server/middleware/auth"; import { ScoreImportRateLimiter } from "server/middleware/rate-limiter"; import { Random20Hex } from "utils/misc"; +import type { ScoreImportJobData } from "lib/score-import/worker/types"; const router: Router = Router({ mergeParams: true }); @@ -19,14 +19,14 @@ router.post( RequirePermissions("submit_score"), ScoreImportRateLimiter, async (req, res) => { - const userIntent = !!req.header("X-User-Intent"); + const userIntent = Boolean(req.header("X-User-Intent")); if (ServerConfig.USE_EXTERNAL_SCORE_IMPORT_WORKER) { const importID = Random20Hex(); const job: ScoreImportJobData<"ir/direct-manual"> = { importID, - userID: req[SYMBOL_TachiAPIAuth].userID!, + userID: req[SYMBOL_TACHI_API_AUTH].userID!, userIntent, importType: "ir/direct-manual", parserArguments: [req.body], @@ -44,17 +44,17 @@ router.post( importID, }, }); - } else { - // Fire the score import and wait for it to finish! - const importResponse = await ExpressWrappedScoreImportMain<"ir/direct-manual">( - req[SYMBOL_TachiAPIAuth].userID!, - userIntent, - "ir/direct-manual", - [req.body] - ); - - return res.status(importResponse.statusCode).json(importResponse.body); } + + // Fire the score import and wait for it to finish! + const importResponse = await ExpressWrappedScoreImportMain<"ir/direct-manual">( + req[SYMBOL_TACHI_API_AUTH].userID!, + userIntent, + "ir/direct-manual", + [req.body] + ); + + return res.status(importResponse.statusCode).json(importResponse.body); } ); diff --git a/server/src/server/router/ir/fervidex/router.ts b/server/src/server/router/ir/fervidex/router.ts index ac173a7d7..00675ac58 100644 --- a/server/src/server/router/ir/fervidex/router.ts +++ b/server/src/server/router/ir/fervidex/router.ts @@ -1,4 +1,4 @@ -import { RequestHandler, Router } from "express"; +import { Router } from "express"; import db from "external/mongo/db"; import { EXT_BISTROVER, @@ -6,13 +6,14 @@ import { MODEL_INFINITAS_2, REV_2DXBMS, } from "lib/constants/ea3id"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper"; import { RequirePermissions } from "server/middleware/auth"; -import { integer, Playtypes } from "tachi-common"; import { UpdateClassIfGreater } from "utils/class"; import { ParseEA3SoftID } from "utils/ea3id"; +import type { RequestHandler } from "express"; +import type { integer, Playtypes } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -23,7 +24,8 @@ const ValidateFervidexHeader: RequestHandler = (req, res, next) => { if (!agent) { logger.debug( - `Rejected fervidex client with no agent from user ${req[SYMBOL_TachiAPIAuth].userID!}.` + `Rejected fervidex client with no agent from user ${req[SYMBOL_TACHI_API_AUTH] + .userID!}.` ); return res.status(400).json({ success: false, @@ -34,7 +36,7 @@ const ValidateFervidexHeader: RequestHandler = (req, res, next) => { if (!agent.startsWith("fervidex/")) { logger.info( `Rejected fervidex client with invalid agent ${agent} from user ${req[ - SYMBOL_TachiAPIAuth + SYMBOL_TACHI_API_AUTH ].userID!}.` ); return res.status(400).json({ @@ -48,7 +50,7 @@ const ValidateFervidexHeader: RequestHandler = (req, res, next) => { if (!versions.every((e) => !Number.isNaN(e))) { logger.info( `Rejected fervidex client with agent ${agent} for NaN-like versions from user ${req[ - SYMBOL_TachiAPIAuth + SYMBOL_TACHI_API_AUTH ].userID!}.` ); return res.status(400).json({ @@ -60,7 +62,7 @@ const ValidateFervidexHeader: RequestHandler = (req, res, next) => { // version.minor if (versions[1] < 3) { logger.debug( - `Rejected outdated fervidex client from user ${req[SYMBOL_TachiAPIAuth].userID}.` + `Rejected outdated fervidex client from user ${req[SYMBOL_TACHI_API_AUTH].userID}.` ); return res.status(400).json({ success: false, @@ -68,11 +70,13 @@ const ValidateFervidexHeader: RequestHandler = (req, res, next) => { }); } - return next(); + next(); }; const RequireInf2ModelHeaderOrForceStatic: RequestHandler = async (req, res, next) => { - const settings = await db["fer-settings"].findOne({ userID: req[SYMBOL_TachiAPIAuth].userID! }); + const settings = await db["fer-settings"].findOne({ + userID: req[SYMBOL_TACHI_API_AUTH].userID!, + }); if (settings && settings.forceStaticImport) { logger.debug(`User ${settings.userID} had forceStaticImport set, allowing request.`); @@ -93,14 +97,15 @@ const RequireInf2ModelHeaderOrForceStatic: RequestHandler = async (req, res, nex } ); - return next(); + next(); + return; } const swModel = req.header("X-Software-Model"); if (!swModel) { logger.debug( - `Rejected empty X-Software-Model from user ${req[SYMBOL_TachiAPIAuth].userID!}.` + `Rejected empty X-Software-Model from user ${req[SYMBOL_TACHI_API_AUTH].userID!}.` ); return res.status(400).json({ success: false, @@ -118,14 +123,14 @@ const RequireInf2ModelHeaderOrForceStatic: RequestHandler = async (req, res, nex }); } } catch (err) { - logger.info(`Invalid softID from ${req[SYMBOL_TachiAPIAuth].userID!}.`, { err }); + logger.info(`Invalid softID from ${req[SYMBOL_TACHI_API_AUTH].userID!}.`, { err }); return res.status(400).json({ success: false, error: `Invalid X-Software-Model.`, }); } - return next(); + next(); }; const supportedExts = [EXT_HEROIC_VERSE, EXT_BISTROVER]; @@ -135,7 +140,7 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => { if (!swModel) { logger.debug( - `Rejected empty X-Software Model from user ${req[SYMBOL_TachiAPIAuth].userID!}.` + `Rejected empty X-Software Model from user ${req[SYMBOL_TACHI_API_AUTH].userID!}.` ); return res.status(400).json({ success: false, @@ -154,13 +159,15 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => { } if (softID.model === MODEL_INFINITAS_2) { - return next(); // allow anything for inf2. + next(); + return; // allow anything for inf2. } if (!supportedExts.includes(softID.ext)) { logger.info( - `Rejected invalid Software Model ${softID.ext} from user ${req[SYMBOL_TachiAPIAuth] - .userID!}.` + `Rejected invalid Software Model ${softID.ext} from user ${req[ + SYMBOL_TACHI_API_AUTH + ].userID!}.` ); return res.status(400).json({ success: false, @@ -175,19 +182,21 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => { }); } - return next(); + next(); }; const ValidateCards: RequestHandler = async (req, res, next) => { - const userID = req[SYMBOL_TachiAPIAuth]!.userID!; + const userID = req[SYMBOL_TACHI_API_AUTH]!.userID!; const cardFilters = await db["fer-settings"].findOne({ userID }); if (!cardFilters || !cardFilters.cards) { - return next(); + next(); + return; } const cardID = req.header("X-Account-Id"); + if (!cardID) { return res.status(400).json({ success: false, @@ -202,7 +211,7 @@ const ValidateCards: RequestHandler = async (req, res, next) => { }); } - return next(); + next(); }; router.use( @@ -233,7 +242,7 @@ router.post("/profile/submit", RequireInf2ModelHeaderOrForceStatic, (req, res) = body: {}, }); - ExpressWrappedScoreImportMain(req[SYMBOL_TachiAPIAuth].userID!, false, "ir/fervidex-static", [ + ExpressWrappedScoreImportMain(req[SYMBOL_TACHI_API_AUTH].userID!, false, "ir/fervidex-static", [ req.body, headers, ]); @@ -261,7 +270,7 @@ router.post("/score/submit", ValidateModelHeader, async (req, res) => { }; const responseData = await ExpressWrappedScoreImportMain( - req[SYMBOL_TachiAPIAuth].userID!, + req[SYMBOL_TACHI_API_AUTH].userID!, true, "ir/fervidex", [req.body, headers] @@ -271,6 +280,7 @@ router.post("/score/submit", ValidateModelHeader, async (req, res) => { // in-air rewrite description to error. // @ts-expect-error Hack! responseData.body.error = responseData.body.description; + // @ts-expect-error Hack! delete responseData.body.description; } @@ -316,7 +326,7 @@ router.post("/class/submit", ValidateModelHeader, async (req, res) => { const playtype: Playtypes["iidx"] = req.body.play_style === 0 ? "SP" : "DP"; const r = await UpdateClassIfGreater( - req[SYMBOL_TachiAPIAuth].userID!, + req[SYMBOL_TACHI_API_AUTH].userID!, "iidx", playtype, "dan", diff --git a/server/src/server/router/ir/kshook/router.ts b/server/src/server/router/ir/kshook/router.ts index a8c46f0c6..6dc98f050 100644 --- a/server/src/server/router/ir/kshook/router.ts +++ b/server/src/server/router/ir/kshook/router.ts @@ -1,5 +1,5 @@ -import { Router } from "express"; import SV6CRouter from "./sv6c/router"; +import { Router } from "express"; const router: Router = Router({ mergeParams: true }); diff --git a/server/src/server/router/ir/kshook/sv6c/router.ts b/server/src/server/router/ir/kshook/sv6c/router.ts index 8fe2e0d17..fa361cb86 100644 --- a/server/src/server/router/ir/kshook/sv6c/router.ts +++ b/server/src/server/router/ir/kshook/sv6c/router.ts @@ -1,9 +1,10 @@ -import { RequestHandler, Router } from "express"; +import { Router } from "express"; import { MODEL_SDVX3_KONASTE } from "lib/constants/ea3id"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper"; import { ParseEA3SoftID } from "utils/ea3id"; +import type { RequestHandler } from "express"; const router: Router = Router({ mergeParams: true }); @@ -14,7 +15,7 @@ const ValidateHeaders: RequestHandler = (req, res, next) => { if (!agent) { logger.debug( - `Rejected KsHook client with no agent from user ${req[SYMBOL_TachiAPIAuth].userID!}.` + `Rejected KsHook client with no agent from user ${req[SYMBOL_TACHI_API_AUTH].userID!}.` ); return res.status(400).json({ success: false, @@ -24,8 +25,9 @@ const ValidateHeaders: RequestHandler = (req, res, next) => { if (!agent.startsWith("kshook/")) { logger.info( - `Rejected KsHook client with invalid agent ${agent} from user ${req[SYMBOL_TachiAPIAuth] - .userID!}.` + `Rejected KsHook client with invalid agent ${agent} from user ${req[ + SYMBOL_TACHI_API_AUTH + ].userID!}.` ); return res.status(400).json({ success: false, @@ -40,7 +42,7 @@ const ValidateHeaders: RequestHandler = (req, res, next) => { if (!softID) { logger.debug( - `received request without X-Software-Model from ${req[SYMBOL_TachiAPIAuth].userID!}.` + `received request without X-Software-Model from ${req[SYMBOL_TACHI_API_AUTH].userID!}.` ); return res.status(400).json({ success: false, @@ -61,14 +63,14 @@ const ValidateHeaders: RequestHandler = (req, res, next) => { }); } } catch (err) { - logger.info(`Invalid softID from ${req[SYMBOL_TachiAPIAuth].userID!}.`, { err }); + logger.info(`Invalid softID from ${req[SYMBOL_TACHI_API_AUTH].userID!}.`, { err }); return res.status(400).json({ success: false, error: `Invalid X-Software-Model.`, }); } - return next(); + next(); }; router.use(ValidateHeaders); @@ -80,7 +82,7 @@ router.use(ValidateHeaders); */ router.post("/score/save", async (req, res) => { const responseData = await ExpressWrappedScoreImportMain( - req[SYMBOL_TachiAPIAuth].userID!, + req[SYMBOL_TACHI_API_AUTH].userID!, true, "ir/kshook-sv6c", [req.body] @@ -90,6 +92,7 @@ router.post("/score/save", async (req, res) => { // in-air rewrite description to error. // @ts-expect-error Hack! responseData.body.error = responseData.body.description; + // @ts-expect-error Hack! delete responseData.body.description; } diff --git a/server/src/server/router/ir/lr2hook/router.ts b/server/src/server/router/ir/lr2hook/router.ts index ad7bc9fd8..42425228d 100644 --- a/server/src/server/router/ir/lr2hook/router.ts +++ b/server/src/server/router/ir/lr2hook/router.ts @@ -1,12 +1,12 @@ import { Router } from "express"; import db from "external/mongo/db"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper"; import { PR_LR2Hook } from "lib/score-import/import-types/ir/lr2hook/parser"; -import { LR2HookScore } from "lib/score-import/import-types/ir/lr2hook/types"; import { RequirePermissions } from "server/middleware/auth"; import prValidate from "server/middleware/prudence-validate"; import { UpdateClassIfGreater } from "utils/class"; +import type { LR2HookScore } from "lib/score-import/import-types/ir/lr2hook/types"; const router: Router = Router({ mergeParams: true }); @@ -19,7 +19,7 @@ router.use(RequirePermissions("submit_score")); */ router.post("/import", async (req, res) => { const importRes = await ExpressWrappedScoreImportMain( - req[SYMBOL_TachiAPIAuth].userID!, + req[SYMBOL_TACHI_API_AUTH].userID!, false, "ir/lr2hook", [req.body] @@ -61,7 +61,7 @@ router.post("/import/course", prValidate(PR_LR2Hook), async (req, res) => { }); } - const userID = req[SYMBOL_TachiAPIAuth]!.userID!; + const userID = req[SYMBOL_TACHI_API_AUTH]!.userID!; const result = await UpdateClassIfGreater( userID, diff --git a/server/src/server/router/ir/router.ts b/server/src/server/router/ir/router.ts index 3a58cd1e1..ec0e2da2d 100644 --- a/server/src/server/router/ir/router.ts +++ b/server/src/server/router/ir/router.ts @@ -1,11 +1,3 @@ -import { Router } from "express"; -import { - FervidexStyleRequireNotGuest, - RequireNotGuest, - SetFervidexStyleRequestPermissions, - SetRequestPermissions, -} from "../../middleware/auth"; -import { RequireBokutachi, RequireKamaitachi } from "../../middleware/type-require"; import barbatosIR from "./barbatos/router"; import beatorajaIR from "./beatoraja/router"; import directManualIR from "./direct-manual/router"; @@ -13,6 +5,14 @@ import fervidexIR from "./fervidex/router"; import ksHookIR from "./kshook/router"; import lr2hookIR from "./lr2hook/router"; import uscIR from "./usc/router"; +import { + FervidexStyleRequireNotGuest, + RequireNotGuest, + SetFervidexStyleRequestPermissions, + SetRequestPermissions, +} from "../../middleware/auth"; +import { RequireBokutachi, RequireKamaitachi } from "../../middleware/type-require"; +import { Router } from "express"; const router: Router = Router({ mergeParams: true }); diff --git a/server/src/server/router/ir/usc/_playtype/router.ts b/server/src/server/router/ir/usc/_playtype/router.ts index cb717a423..0e3cc5681 100644 --- a/server/src/server/router/ir/usc/_playtype/router.ts +++ b/server/src/server/router/ir/usc/_playtype/router.ts @@ -1,9 +1,10 @@ -import { RequestHandler, Router } from "express"; +import { CreatePOSTScoresResponseBody, TachiScoreToServerScore } from "./usc"; +import { Router } from "express"; import db from "external/mongo/db"; import { CDNStoreOrOverwrite } from "lib/cdn/cdn"; import { GetUSCIRReplayURL } from "lib/cdn/url-format"; import { ONE_MEGABYTE } from "lib/constants/filesize"; -import { SYMBOL_TachiAPIAuth, SYMBOL_TachiData } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH, SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import { USCIR_MAX_LEADERBOARD_N } from "lib/constants/usc-ir"; import CreateLogCtx from "lib/logger/logger"; import { AssertStrAsPositiveNonZeroInt } from "lib/score-import/framework/common/string-asserts"; @@ -12,17 +13,18 @@ import { ServerConfig, TachiConfig } from "lib/setup/config"; import p from "prudence"; import { RequirePermissions } from "server/middleware/auth"; import { CreateMulterSingleUploadMiddleware } from "server/middleware/multer-upload"; -import { +import { FormatPrError } from "utils/prudence"; +import { AssignToReqTachiData } from "utils/req-tachi-data"; +import type { USCClientChart } from "./types"; +import type { RequestHandler } from "express"; +import type { ChartDocument, ImportDocument, PBScoreDocument, Playtypes, SuccessfulAPIResponse, } from "tachi-common"; -import { FormatPrError } from "utils/prudence"; -import { AssignToReqTachiData } from "utils/req-tachi-data"; -import { USCClientChart } from "./types"; -import { CreatePOSTScoresResponseBody, TachiScoreToServerScore } from "./usc"; + const logger = CreateLogCtx(__filename); const router: Router = Router({ mergeParams: true }); @@ -68,9 +70,9 @@ const ValidateUSCRequest: RequestHandler = async (req, res, next) => { }); } - req[SYMBOL_TachiAPIAuth] = uscAuthDoc; + req[SYMBOL_TACHI_API_AUTH] = uscAuthDoc; - return next(); + next(); }; router.use((req, res, next) => { @@ -81,10 +83,11 @@ router.use((req, res, next) => { }); } - return next(); + next(); }); router.use(ValidateUSCRequest); + // This is an implementation of the USCIR spec as per https://uscir.readthedocs.io. // This specification always returns 200 OK, regardless of whether the result was okay // as the HTTP code is used to determine whether the server received the request properly, @@ -119,7 +122,7 @@ const RetrieveChart: RequestHandler = async (req, res, next) => { | undefined, }); - return next(); + next(); }; /** @@ -128,7 +131,7 @@ const RetrieveChart: RequestHandler = async (req, res, next) => { * @name GET /ir/usc/:playtype/charts/:chartHash */ router.get("/charts/:chartHash", RetrieveChart, (req, res) => { - const chart = req[SYMBOL_TachiData]!.uscChartDoc; + const chart = req[SYMBOL_TACHI_DATA]!.uscChartDoc; if (!chart) { return res.status(200).json({ @@ -150,7 +153,7 @@ router.get("/charts/:chartHash", RetrieveChart, (req, res) => { * @name GET /ir/usc/:playtype/charts/:chartHash/record */ router.get("/charts/:chartHash/record", RetrieveChart, async (req, res) => { - const chart = req[SYMBOL_TachiData]!.uscChartDoc; + const chart = req[SYMBOL_TACHI_DATA]!.uscChartDoc; if (!chart) { return res.status(200).json({ @@ -190,7 +193,7 @@ router.get("/charts/:chartHash/record", RetrieveChart, async (req, res) => { * @name GET /ir/usc/:playtype/charts/:chartHash/leaderboard */ router.get("/charts/:chartHash/leaderboard", RetrieveChart, async (req, res) => { - const chart = req[SYMBOL_TachiData]!.uscChartDoc; + const chart = req[SYMBOL_TACHI_DATA]!.uscChartDoc; if (!chart) { return res.status(200).json({ @@ -251,7 +254,7 @@ router.get("/charts/:chartHash/leaderboard", RetrieveChart, async (req, res) => }, limit: n, } - )) as PBScoreDocument<"usc:Controller" | "usc:Keyboard">[]; + )) as Array>; const serverScores = await Promise.all(bestScores.map(TachiScoreToServerScore)); @@ -305,7 +308,7 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => { playtype, })) as ChartDocument<"usc:Controller" | "usc:Keyboard"> | null; - const userID = req[SYMBOL_TachiAPIAuth]!.userID!; + const userID = req[SYMBOL_TACHI_API_AUTH]!.userID!; const importRes = await ExpressWrappedScoreImportMain(userID, false, "ir/usc", [ req.body, @@ -417,7 +420,7 @@ router.post( // Otherwise, anyone could overwrite anyone elses // score replays! const correspondingScore = await db.scores.findOne({ - userID: req[SYMBOL_TachiAPIAuth]!.userID!, + userID: req[SYMBOL_TACHI_API_AUTH]!.userID!, scoreID: req.body.identifier, game: "usc", }); diff --git a/server/src/server/router/ir/usc/_playtype/types.ts b/server/src/server/router/ir/usc/_playtype/types.ts index 861c67df0..708dd173a 100644 --- a/server/src/server/router/ir/usc/_playtype/types.ts +++ b/server/src/server/router/ir/usc/_playtype/types.ts @@ -1,4 +1,4 @@ -import { integer } from "tachi-common"; +import type { integer } from "tachi-common"; export interface USCServerScore { score: integer; @@ -8,8 +8,8 @@ export interface USCServerScore { near: integer; error: integer; ranking: integer; - gaugeMod: "NORMAL" | "HARD" | "PERMISSIVE"; - noteMod: "NORMAL" | "MIRROR" | "RANDOM" | "MIR-RAN"; + gaugeMod: "HARD" | "NORMAL" | "PERMISSIVE"; + noteMod: "MIR-RAN" | "MIRROR" | "NORMAL" | "RANDOM"; username: string; } @@ -28,7 +28,7 @@ export interface USCClientScore { gaugeOpt: integer; mirror: boolean; random: boolean; - autoFlags: integer; //??? + autoFlags: integer; // ??? }; windows: { perfect: number; diff --git a/server/src/server/router/ir/usc/_playtype/usc.ts b/server/src/server/router/ir/usc/_playtype/usc.ts index c0da2018d..c8fe723a5 100644 --- a/server/src/server/router/ir/usc/_playtype/usc.ts +++ b/server/src/server/router/ir/usc/_playtype/usc.ts @@ -1,10 +1,11 @@ import db from "external/mongo/db"; import { USCIR_ADJACENT_SCORE_N } from "lib/constants/usc-ir"; import CreateLogCtx from "lib/logger/logger"; -import { ChartDocument, integer, PBScoreDocument, ScoreDocument } from "tachi-common"; import { MStoS } from "utils/misc"; import { GetPBOnChart, GetServerRecordOnChart } from "utils/scores"; -import { USCServerScore } from "./types"; +import type { USCServerScore } from "./types"; +import type { ChartDocument, integer, PBScoreDocument, ScoreDocument } from "tachi-common"; + const logger = CreateLogCtx(__filename); export const TACHI_LAMP_TO_USC: Record< @@ -131,7 +132,7 @@ export async function CreatePOSTScoresResponseBody( limit: USCIR_ADJACENT_SCORE_N, sort: { "rankingData.rank": -1 }, } - )) as PBScoreDocument<"usc:Controller" | "usc:Keyboard">[]; + )) as Array>; // The specification enforces that we return them in // ascending order, though, so we reverse this after @@ -157,7 +158,7 @@ export async function CreatePOSTScoresResponseBody( limit: USCIR_ADJACENT_SCORE_N, sort: { "rankingData.rank": 1 }, } - )) as PBScoreDocument<"usc:Controller" | "usc:Keyboard">[]; + )) as Array>; const [score, serverRecord, adjacentAbove, adjacentBelow] = await Promise.all([ TachiScoreToServerScore(scorePB), @@ -182,7 +183,7 @@ export async function CreatePOSTScoresResponseBody( return { score, serverRecord, - isServerRecord: scorePB.userID === ktServerRecord?.userID, + isServerRecord: scorePB.userID === ktServerRecord.userID, isPB: scorePB.composedFrom.scorePB === scoreID, sendReplay: originalScore.scoreID, adjacentAbove, @@ -193,8 +194,8 @@ export async function CreatePOSTScoresResponseBody( export interface POSTScoresResponseBody { score: USCServerScore; serverRecord: USCServerScore; - adjacentAbove: USCServerScore[]; - adjacentBelow: USCServerScore[]; + adjacentAbove: Array; + adjacentBelow: Array; isPB: boolean; isServerRecord: boolean; sendReplay: string; diff --git a/server/src/server/router/ir/usc/router.ts b/server/src/server/router/ir/usc/router.ts index 274a7e4a2..d3d99d11e 100644 --- a/server/src/server/router/ir/usc/router.ts +++ b/server/src/server/router/ir/usc/router.ts @@ -1,5 +1,5 @@ -import { Router } from "express"; import uscirRouter from "./_playtype/router"; +import { Router } from "express"; const router: Router = Router({ mergeParams: true }); diff --git a/server/src/server/router/router.ts b/server/src/server/router/router.ts index c9c6d04d4..5c2843b6c 100644 --- a/server/src/server/router/router.ts +++ b/server/src/server/router/router.ts @@ -1,9 +1,9 @@ -import { Router } from "express"; -import { UpdateLastSeen } from "server/middleware/update-last-seen"; -import { RejectIfBanned, SetRequestPermissions } from "../middleware/auth"; -import { NormalRateLimitMiddleware } from "../middleware/rate-limiter"; import apiRouterV1 from "./api/v1/router"; import irRouter from "./ir/router"; +import { RejectIfBanned, SetRequestPermissions } from "../middleware/auth"; +import { NormalRateLimitMiddleware } from "../middleware/rate-limiter"; +import { Router } from "express"; +import { UpdateLastSeen } from "server/middleware/update-last-seen"; const router: Router = Router({ mergeParams: true }); diff --git a/server/src/server/server.ts b/server/src/server/server.ts index 201dbff4c..09ba55b80 100644 --- a/server/src/server/server.ts +++ b/server/src/server/server.ts @@ -1,13 +1,14 @@ import connectRedis from "connect-redis"; -import express, { Express } from "express"; +import express from "express"; +import type { Express } from "express"; import "express-async-errors"; import expressSession from "express-session"; import { RedisClient } from "external/redis/redis"; import helmet from "helmet"; -import { SYMBOL_TachiAPIAuth } from "lib/constants/tachi"; +import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; import { Environment, ServerConfig, TachiConfig } from "lib/setup/config"; -import { integer } from "tachi-common"; +import type { integer } from "tachi-common"; import { RequestLoggerMiddleware } from "./middleware/request-logger"; import mainRouter from "./router/router"; @@ -18,6 +19,7 @@ let store; if (Environment.nodeEnv !== "test") { logger.info("Connecting ExpressSession to Redis.", { bootInfo: true }); const RedisStore = connectRedis(expressSession); + store = new RedisStore({ host: "localhost", port: 6379, @@ -70,6 +72,7 @@ if (Environment.nodeEnv !== "production" && ServerConfig.CLIENT_DEV_SERVER) { bootInfo: true, }); } + app.use(helmet()); } @@ -102,7 +105,7 @@ app.use((req, res, next) => { req.body = {}; } - return next(); + next(); }); app.use(RequestLoggerMiddleware); @@ -141,10 +144,11 @@ interface ExpressJSONErr extends SyntaxError { const MAIN_ERR_HANDLER: express.ErrorRequestHandler = (err, req, res, next) => { if (err instanceof SyntaxError) { const expErr: ExpressJSONErr = err as ExpressJSONErr; + if (expErr.status === 400 && "body" in expErr) { logger.info(`JSON Parsing Error?`, { url: req.originalUrl, - userID: req[SYMBOL_TachiAPIAuth]?.userID, + userID: req[SYMBOL_TACHI_API_AUTH].userID, }); return res.status(400).send({ success: false, description: err.message }); } diff --git a/server/src/test-utils/api-common.ts b/server/src/test-utils/api-common.ts index 26bf48827..45dcf83bf 100644 --- a/server/src/test-utils/api-common.ts +++ b/server/src/test-utils/api-common.ts @@ -1,13 +1,13 @@ -import db from "external/mongo/db"; -import { APIPermissions } from "tachi-common"; -import t from "tap"; import mockApi from "./mock-api"; import ResetDBState from "./resets"; +import db from "external/mongo/db"; +import t from "tap"; +import type { APIPermissions } from "tachi-common"; export function RequireAuthPerms( url: string, - perms: APIPermissions | APIPermissions[], - method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE" = "GET" + perms: APIPermissions | Array, + method: "DELETE" | "GET" | "PATCH" | "POST" | "PUT" = "GET" ) { t.test(`Testing permissions for ${method} ${url} [${perms}]`, async (t) => { const m = method.toLowerCase() as Lowercase; diff --git a/server/src/test-utils/asserts.ts b/server/src/test-utils/asserts.ts index 492ae9998..3e1a513cc 100644 --- a/server/src/test-utils/asserts.ts +++ b/server/src/test-utils/asserts.ts @@ -1,4 +1,6 @@ -import Prudence, { PrudenceSchema } from "prudence"; +import Prudence from "prudence"; +import type { PrudenceSchema } from "prudence"; + export function isApproximately( t: Tap.Test, number: number, diff --git a/server/src/test-utils/cleanup.ts b/server/src/test-utils/cleanup.ts index d3fba002f..ceec739b3 100644 --- a/server/src/test-utils/cleanup.ts +++ b/server/src/test-utils/cleanup.ts @@ -1,9 +1,9 @@ -import { CloseMongoConnection } from "external/mongo/db"; -import { CloseRedisPubSub } from "external/redis/redis-IPC"; -import { CloseRedisConnection } from "external/redis/redis"; import { CloseServerConnection } from "./mock-api"; -import { CloseScoreImportQueue } from "lib/score-import/worker/queue"; import { WriteSnapshotData } from "./single-process-snapshot"; +import { CloseMongoConnection } from "external/mongo/db"; +import { CloseRedisConnection } from "external/redis/redis"; +import { CloseRedisPubSub } from "external/redis/redis-IPC"; +import { CloseScoreImportQueue } from "lib/score-import/worker/queue"; export async function CleanUpAfterTests() { if (process.env.TAP_SNAPSHOT) { diff --git a/server/src/test-utils/fake-auth.ts b/server/src/test-utils/fake-auth.ts index 5653ffbb8..95c33e89a 100644 --- a/server/src/test-utils/fake-auth.ts +++ b/server/src/test-utils/fake-auth.ts @@ -1,9 +1,9 @@ +import ResetDBState from "./resets"; import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; import { ClearTestingRateLimitCache } from "server/middleware/rate-limiter"; -import supertest from "supertest"; import { ALL_PERMISSIONS } from "tachi-common"; -import ResetDBState from "./resets"; +import type supertest from "supertest"; const logger = CreateLogCtx(__filename); @@ -23,7 +23,7 @@ export async function CreateFakeAuthCookie(mockApi: supertest.SuperTest; } // my local dev env hates this part because of pnpm diff --git a/server/src/test-utils/misc.ts b/server/src/test-utils/misc.ts index e487dc982..c912d6a33 100644 --- a/server/src/test-utils/misc.ts +++ b/server/src/test-utils/misc.ts @@ -1,5 +1,14 @@ -import deepmerge from "deepmerge"; import { + FakeGameSettings, + FakeImport, + FakeNotification, + FakeOtherUser, + TestingIIDXSPScore, + TestingIIDXSPScorePB, + TestingSDVXScore, +} from "./test-data"; +import deepmerge from "deepmerge"; +import type { Game, ImportDocument, integer, @@ -10,21 +19,13 @@ import { ScoreDocument, UGPTSettings, } from "tachi-common"; -import { - FakeGameSettings, - FakeImport, - FakeNotification, - FakeOtherUser, - TestingIIDXSPScore, - TestingIIDXSPScorePB, - TestingSDVXScore, -} from "./test-data"; /** * Async Generator To Array */ export async function agta(ag: AsyncIterable | Iterable) { const a = []; + for await (const el of ag) { a.push(el); } @@ -39,7 +40,7 @@ export function dmf(base: T, modifant: Partial): T { return deepmerge(base, modifant, { // The new array should replace the former one, instead of joining them together. arrayMerge: (originalArray, newArray) => newArray, - }) as T; + }); } /** @@ -49,17 +50,12 @@ export function dmf(base: T, modifant: Partial): T { * @param userID - The userID this fake user should have. */ export function mkFakeUser(userID: integer, modifant: Partial = {}) { - return dmf( - FakeOtherUser, - Object.assign( - { - id: userID, - username: `user${userID}`, - usernameLowercase: `user${userID}`, - }, - modifant - ) - ); + return dmf(FakeOtherUser, { + id: userID, + username: `user${userID}`, + usernameLowercase: `user${userID}`, + ...modifant, + }); } export function mkFakeGameSettings( @@ -68,17 +64,12 @@ export function mkFakeGameSettings( playtype: Playtype, modifant: Partial = {} ) { - return dmf( - FakeGameSettings, - Object.assign( - { - userID, - game, - playtype, - }, - modifant - ) - ); + return dmf(FakeGameSettings, { + userID, + game, + playtype, + ...modifant, + }); } export function mkFakeImport(modifant: Partial = {}) { diff --git a/server/src/test-utils/mock-api.ts b/server/src/test-utils/mock-api.ts index b5a9593f3..065082611 100644 --- a/server/src/test-utils/mock-api.ts +++ b/server/src/test-utils/mock-api.ts @@ -1,6 +1,6 @@ +import server from "../server/server"; import CreateLogCtx from "lib/logger/logger"; import supertest from "supertest"; -import server from "../server/server"; const logger = CreateLogCtx(__filename); diff --git a/server/src/test-utils/mock-fetch.ts b/server/src/test-utils/mock-fetch.ts index 16f4bf380..6d6d24ceb 100644 --- a/server/src/test-utils/mock-fetch.ts +++ b/server/src/test-utils/mock-fetch.ts @@ -1,7 +1,7 @@ // We know! these are mock functions. /* eslint-disable require-await */ -import { Response } from "node-fetch"; -import { NodeFetch } from "utils/fetch"; +import type { Response } from "node-fetch"; +import type { NodeFetch } from "utils/fetch"; /** * Creates a basic Fetch function used for statusCode checks. diff --git a/server/src/test-utils/resets.ts b/server/src/test-utils/resets.ts index 3fb6c6af3..cf5f89004 100644 --- a/server/src/test-utils/resets.ts +++ b/server/src/test-utils/resets.ts @@ -2,13 +2,14 @@ * Resets the state of the database. */ import db from "external/mongo/db"; +import { SetIndexes } from "external/mongo/indexes"; +import CreateLogCtx from "lib/logger/logger"; +import { Environment, ServerConfig } from "lib/setup/config"; +import rimraf from "rimraf"; import fs from "fs"; import path from "path"; -import CreateLogCtx from "lib/logger/logger"; + // im installing an entire library for rm rf... -import rimraf from "rimraf"; -import { SetIndexes } from "external/mongo/indexes"; -import { Environment, ServerConfig } from "lib/setup/config"; if (ServerConfig.CDN_CONFIG.SAVE_LOCATION.TYPE !== "LOCAL_FILESYSTEM") { throw new Error( @@ -21,10 +22,10 @@ const logger = CreateLogCtx(__filename); const DATA_DIR = path.join(__dirname, "./mock-db"); // eslint-disable-next-line @typescript-eslint/no-explicit-any -const CACHE: Record = {}; +const CACHE: Record> = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any -async function ResetState(data: any[], collection: any) { +async function ResetState(data: Array, collection: any) { await collection.remove({}); await collection.insert(data); @@ -32,6 +33,7 @@ async function ResetState(data: any[], collection: any) { function GetAndCache(filename: string, fileLoc: string) { let collection; + if (filename.startsWith("songs-")) { // @ts-expect-error it's right, but we know what we're doing! collection = db.songs[filename.split("-")[1]]; @@ -64,10 +66,11 @@ function GetAndCache(filename: string, fileLoc: string) { return { data, collection }; } -let CACHE_FILENAMES: string[]; +let CACHE_FILENAMES: Array; export default async function ResetDBState() { let files; + if (CACHE_FILENAMES) { files = CACHE_FILENAMES; } else { @@ -101,6 +104,7 @@ export function ResetCDN() { if (err) { reject(err); } + resolve(); }); }); diff --git a/server/src/test-utils/server.ts b/server/src/test-utils/server.ts index 2ec386f46..f8806cf6f 100644 --- a/server/src/test-utils/server.ts +++ b/server/src/test-utils/server.ts @@ -1,5 +1,5 @@ -import supertest from "supertest"; import server from "../server/server"; +import supertest from "supertest"; const mockServer = supertest(server); diff --git a/server/src/test-utils/setup.ts b/server/src/test-utils/setup.ts index 06cbafe96..0bbbf1323 100644 --- a/server/src/test-utils/setup.ts +++ b/server/src/test-utils/setup.ts @@ -1,5 +1,5 @@ -import { monkDB } from "external/mongo/db"; import { SetIndexesForDB } from "./resets"; +import { monkDB } from "external/mongo/db"; SetIndexesForDB() .then(monkDB.close) diff --git a/server/src/test-utils/single-process-tap.ts b/server/src/test-utils/single-process-tap.ts index 349e134bb..433dc56b5 100644 --- a/server/src/test-utils/single-process-tap.ts +++ b/server/src/test-utils/single-process-tap.ts @@ -1,8 +1,8 @@ import "external/mongo/db"; -import glob from "glob"; -import path from "path"; -import t from "tap"; import { CleanUpAfterTests } from "./cleanup"; +import glob from "glob"; +import t from "tap"; +import path from "path"; const files = glob.sync(path.join(__dirname, "../../", "**/*.test.ts")); diff --git a/server/src/test-utils/test-data.ts b/server/src/test-utils/test-data.ts index f4810cbc0..7c0c7147e 100644 --- a/server/src/test-utils/test-data.ts +++ b/server/src/test-utils/test-data.ts @@ -1,6 +1,5 @@ import dm from "deepmerge"; import db from "external/mongo/db"; -import fs from "fs"; import { USC_DEFAULT_HOLD, USC_DEFAULT_MISS, @@ -8,13 +7,15 @@ import { USC_DEFAULT_PERFECT, USC_DEFAULT_SLAM, } from "lib/constants/usc-ir"; -import { DryScore } from "lib/score-import/framework/common/types"; -import { BarbatosScore } from "lib/score-import/import-types/ir/barbatos/types"; -import { KsHookSV6CScore } from "lib/score-import/import-types/ir/kshook-sv6c/types"; -import { LR2HookScore } from "lib/score-import/import-types/ir/lr2hook/types"; +import { ApplyNTimes, RFA } from "utils/misc"; +import fs from "fs"; import path from "path"; -import { USCClientScore } from "server/router/ir/usc/_playtype/types"; -import { +import type { DryScore } from "lib/score-import/framework/common/types"; +import type { BarbatosScore } from "lib/score-import/import-types/ir/barbatos/types"; +import type { KsHookSV6CScore } from "lib/score-import/import-types/ir/kshook-sv6c/types"; +import type { LR2HookScore } from "lib/score-import/import-types/ir/lr2hook/types"; +import type { USCClientScore } from "server/router/ir/usc/_playtype/types"; +import type { ChartDocument, FolderDocument, GoalDocument, @@ -29,7 +30,6 @@ import { SongDocument, UGPTSettings, } from "tachi-common"; -import { ApplyNTimes, RFA } from "utils/misc"; const file = (name: string) => path.join(__dirname, "/test-data", name); @@ -523,14 +523,14 @@ export const TestingIIDXSPMilestone: MilestoneDocument = { ], }; -export const IIDXSPMilestoneGoals: GoalDocument[] = [ +export const IIDXSPMilestoneGoals: Array = [ dm(HC511Goal, { goalID: "eg_goal_1" }) as GoalDocument, dm(HC511Goal, { goalID: "eg_goal_2", criteria: { value: 2 } }), dm(HC511Goal, { goalID: "eg_goal_3", criteria: { key: "scoreData.score", value: 300 } }), dm(HC511Goal, { goalID: "eg_goal_4", criteria: { key: "scoreData.score", value: 1100 } }), ]; -export const IIDXSPMilestoneGoalSubs: GoalSubscriptionDocument[] = [ +export const IIDXSPMilestoneGoalSubs: Array = [ dm(HC511UserGoal, { goalID: "eg_goal_1" }) as GoalSubscriptionDocument, dm(HC511UserGoal, { goalID: "eg_goal_2" }) as GoalSubscriptionDocument, dm(HC511UserGoal, { goalID: "eg_goal_3" }) as GoalSubscriptionDocument, @@ -550,7 +550,7 @@ export const TestingIIDXSPMilestoneSub: MilestoneSubscriptionDocument = { wasInstantlyAchieved: false, }; -let KTDATA_CACHE: { songs: unknown[]; charts: unknown[] } | undefined; +let KTDATA_CACHE: { songs: Array; charts: Array } | undefined; export async function LoadTachiIIDXData() { let songs; diff --git a/server/src/utils/calculations/recalc-scores.ts b/server/src/utils/calculations/recalc-scores.ts index 2f91b97b6..83c400b61 100644 --- a/server/src/utils/calculations/recalc-scores.ts +++ b/server/src/utils/calculations/recalc-scores.ts @@ -1,15 +1,16 @@ /* eslint-disable no-await-in-loop */ +import deepmerge from "deepmerge"; import db from "external/mongo/db"; -import { Game, integer, Playtype, Playtypes, PublicUserDocument } from "tachi-common"; import CreateLogCtx from "lib/logger/logger"; import { CreateCalculatedData } from "lib/score-import/framework/calculated-data/calculated-data"; -import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/user-game-stats/update-ugs"; import { GetAndUpdateUsersGoals } from "lib/score-import/framework/goals/goals"; import { UpdateUsersMilestones } from "lib/score-import/framework/milestones/milestones"; -import { EfficientDBIterate } from "utils/efficient-db-iterate"; 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 deepmerge from "deepmerge"; +import type { Game, integer, Playtype, PublicUserDocument } from "tachi-common"; const logger = CreateLogCtx(__filename); @@ -71,13 +72,7 @@ export async function RecalcAllScores(filter = {}) { const userID = Number(strUserID); - await UpdateUsersGamePlaytypeStats( - game as Game, - playtype as Playtype, - userID, - null, - logger - ); + await UpdateUsersGamePlaytypeStats(game, playtype, userID, null, logger); const goalInfo = await GetAndUpdateUsersGoals(game, userID, chartIDs, logger); @@ -87,8 +82,8 @@ export async function RecalcAllScores(filter = {}) { logger.info(`Done!`); } -export async function UpdateAllPBs(userIDs?: integer[], filter = {}) { - let allUsers: PublicUserDocument[]; +export async function UpdateAllPBs(userIDs?: Array, filter = {}) { + let allUsers: Array; if (!userIDs) { allUsers = await db.users.find({}); diff --git a/server/src/utils/calculations/recalc-sessions.ts b/server/src/utils/calculations/recalc-sessions.ts index 8a6733bd6..35af43646 100644 --- a/server/src/utils/calculations/recalc-sessions.ts +++ b/server/src/utils/calculations/recalc-sessions.ts @@ -8,6 +8,7 @@ const logger = CreateLogCtx(__filename); export async function RecalcSessions(filter = {}) { const allSessions = await db.sessions.find(filter); + logger.info(`Recalcing ${allSessions.length} sessions.`); for (const session of allSessions) { @@ -19,6 +20,7 @@ export async function RecalcSessions(filter = {}) { ); let c; + try { c = CreateSessionCalcData(session.game, session.playtype, scores); } catch (err) { diff --git a/server/src/utils/class.ts b/server/src/utils/class.ts index aae55e1f0..15588cb43 100644 --- a/server/src/utils/class.ts +++ b/server/src/utils/class.ts @@ -2,8 +2,8 @@ import db from "external/mongo/db"; import { CreateGameSettings } from "lib/game-settings/create-game-settings"; import CreateLogCtx from "lib/logger/logger"; import { EmitWebhookEvent } from "lib/webhooks/webhooks"; -import { Game, IDStrings, integer, Playtype, UserGameStats } from "tachi-common"; -import { GameClassSets } from "tachi-common/js/game-classes"; +import type { Game, IDStrings, integer, Playtype, UserGameStats } from "tachi-common"; +import type { GameClassSets } from "tachi-common/js/game-classes"; const logger = CreateLogCtx(__filename); @@ -86,19 +86,19 @@ export async function UpdateClassIfGreater( }); return null; - } else { - EmitWebhookEvent({ - type: "class-update/v1", - content: { - userID, - new: classVal, - old: userGameStats!.classes[classSet]!, - set: classSet, - game, - playtype, - }, - }); - - return true; } + + EmitWebhookEvent({ + type: "class-update/v1", + content: { + userID, + new: classVal, + old: userGameStats!.classes[classSet]!, + set: classSet, + game, + playtype, + }, + }); + + return true; } diff --git a/server/src/utils/db.ts b/server/src/utils/db.ts index fbc3a87df..ec89a392b 100644 --- a/server/src/utils/db.ts +++ b/server/src/utils/db.ts @@ -1,8 +1,8 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; -import { FilterQuery } from "mongodb"; -import { - FormatChart, +import { FormatChart } from "tachi-common"; +import type { FilterQuery } from "mongodb"; +import type { Game, GoalDocument, GoalSubscriptionDocument, @@ -13,6 +13,7 @@ import { PBScoreDocument, ScoreDocument, } from "tachi-common"; + const logger = CreateLogCtx(__filename); export async function GetNextCounterValue(counterName: string): Promise { @@ -66,7 +67,7 @@ export async function DecrementCounterValue(counterName: string): Promise, game: Game ) { const [songs, charts] = await Promise.all([ @@ -177,13 +178,11 @@ export async function GetRecentlyAchievedGoals( baseQuery: FilterQuery, limit = 100 ) { - const query = Object.assign( - { - wasInstantlyAchieved: false, - achieved: true, - }, - baseQuery - ); + const query = { + wasInstantlyAchieved: false, + achieved: true, + ...baseQuery, + }; const goalSubs = await db["goal-subs"].find(query, { sort: { @@ -218,14 +217,12 @@ export async function GetRecentlyInteractedGoals( baseQuery: FilterQuery, limit = 100 ) { - const query = Object.assign( - { - wasInstantlyAchieved: false, - achieved: false, - lastInteraction: { $ne: null }, - }, - baseQuery - ); + const query = { + wasInstantlyAchieved: false, + achieved: false, + lastInteraction: { $ne: null }, + ...baseQuery, + }; const goalSubs = await db["goal-subs"].find(query, { sort: { @@ -260,13 +257,11 @@ export async function GetRecentlyAchievedMilestones( baseQuery: FilterQuery, limit = 100 ) { - const query = Object.assign( - { - wasInstantlyAchieved: false, - achieved: true, - }, - baseQuery - ); + const query = { + wasInstantlyAchieved: false, + achieved: true, + ...baseQuery, + }; const milestoneSubs = await db["milestone-subs"].find(query, { sort: { @@ -301,14 +296,12 @@ export async function GetRecentlyInteractedMilestones( baseQuery: FilterQuery, limit = 100 ) { - const query = Object.assign( - { - lastInteraction: { $ne: null }, - achieved: false, - wasInstantlyAchieved: false, - }, - baseQuery - ); + const query = { + lastInteraction: { $ne: null }, + achieved: false, + wasInstantlyAchieved: false, + ...baseQuery, + }; const milestoneSubs = await db["milestone-subs"].find(query, { sort: { @@ -335,8 +328,8 @@ export async function GetRecentlyInteractedMilestones( export async function GetMostSubscribedGoals( query: FilterQuery, limit = 100 -): Promise<(GoalDocument & { __subscriptions: integer })[]> { - const mostSubscribedGoals = (await db["goal-subs"].aggregate([ +): Promise> { + const mostSubscribedGoals = await db["goal-subs"].aggregate([ { $match: query, }, @@ -370,7 +363,7 @@ export async function GetMostSubscribedGoals( { $unset: "goal._id", }, - ])) as { goal: GoalDocument; subscriptions: integer }[]; + ]); return mostSubscribedGoals.map((e) => ({ __subscriptions: e.subscriptions, @@ -381,8 +374,8 @@ export async function GetMostSubscribedGoals( export async function GetMostSubscribedMilestones( query: FilterQuery, limit = 100 -): Promise<(MilestoneDocument & { __subscriptions: integer })[]> { - const mostSubscribedMilesones = (await db["milestone-subs"].aggregate([ +): Promise> { + const mostSubscribedMilesones = await db["milestone-subs"].aggregate([ { $match: query, }, @@ -416,7 +409,7 @@ export async function GetMostSubscribedMilestones( { $unset: "milestone._id", }, - ])) as { milestone: MilestoneDocument; subscriptions: integer }[]; + ]); return mostSubscribedMilesones.map((e) => ({ __subscriptions: e.subscriptions, diff --git a/server/src/utils/ea3id.ts b/server/src/utils/ea3id.ts index e3bb794cb..70e057a03 100644 --- a/server/src/utils/ea3id.ts +++ b/server/src/utils/ea3id.ts @@ -5,7 +5,7 @@ export function ParseEA3SoftID(ver: string) { throw new Error(`Invalid Version Code. Had ${a.length} components.`); } - if (!ver.match(/^[A-Z0-9]{3}:[A-Z]:[A-Z]:[A-Z]:[0-9]{10}$/u)) { + if (!/^[A-Z0-9]{3}:[A-Z]:[A-Z]:[A-Z]:[0-9]{10}$/u.exec(ver)) { throw new Error(`Invalid Version Code.`); } diff --git a/server/src/utils/efficient-db-iterate.ts b/server/src/utils/efficient-db-iterate.ts index b18ac26de..de168d3de 100644 --- a/server/src/utils/efficient-db-iterate.ts +++ b/server/src/utils/efficient-db-iterate.ts @@ -1,13 +1,13 @@ /* eslint-disable no-await-in-loop */ import CreateLogCtx from "lib/logger/logger"; -import { ICollection } from "monk"; +import type { ICollection } from "monk"; const logger = CreateLogCtx(__filename); export async function EfficientDBIterate( collection: ICollection, callbackFn: (c: T) => Promise, - saveOp: (c: R[]) => Promise, + saveOp: (c: Array) => Promise, // eslint-disable-next-line @typescript-eslint/ban-types filter: object = {}, bucketSize = 10_000 @@ -26,7 +26,7 @@ export async function EfficientDBIterate( const rDocs = await Promise.all(docs.map(callbackFn)); - i += bucketSize; + i = i + bucketSize; await saveOp(rDocs); } diff --git a/server/src/utils/fetch.ts b/server/src/utils/fetch.ts index 22037836f..35d3ea5e1 100644 --- a/server/src/utils/fetch.ts +++ b/server/src/utils/fetch.ts @@ -1,5 +1,6 @@ import { Environment } from "lib/setup/config"; -import nodeFetch, { RequestInfo, RequestInit, Response } from "node-fetch"; +import nodeFetch from "node-fetch"; +import type { RequestInfo, RequestInit, Response } from "node-fetch"; const fetch = Environment.nodeEnv === "test" diff --git a/server/src/utils/folder.ts b/server/src/utils/folder.ts index b97644321..800aae547 100644 --- a/server/src/utils/folder.ts +++ b/server/src/utils/folder.ts @@ -3,8 +3,8 @@ import db from "external/mongo/db"; import fjsh from "fast-json-stable-hash"; import CreateLogCtx from "lib/logger/logger"; import { TachiConfig } from "lib/setup/config"; -import { FilterQuery } from "mongodb"; -import { +import type { FilterQuery } from "mongodb"; +import type { ChartDocument, FolderDocument, Game, @@ -26,19 +26,19 @@ export async function ResolveFolderToCharts( folder: FolderDocument, filter: FilterQuery, getSongs: true -): Promise<{ songs: SongDocument[]; charts: ChartDocument[] }>; +): Promise<{ songs: Array; charts: Array }>; export async function ResolveFolderToCharts( folder: FolderDocument, filter?: FilterQuery, getSongs?: false -): Promise<{ charts: ChartDocument[] }>; +): Promise<{ charts: Array }>; export async function ResolveFolderToCharts( folder: FolderDocument, filter: FilterQuery = {}, getSongs = false -): Promise<{ songs?: SongDocument[]; charts: ChartDocument[] }> { - let songs: SongDocument[] | null = null; - let charts: ChartDocument[]; +): Promise<{ songs?: Array; charts: Array }> { + let songs: Array | null = null; + let charts: Array; if (folder.type === "static") { charts = await db.charts[folder.game].find( @@ -115,17 +115,17 @@ export async function GetFolderCharts( folder: FolderDocument, filter: FilterQuery, getSongs: true -): Promise<{ songs: SongDocument[]; charts: ChartDocument[] }>; +): Promise<{ songs: Array; charts: Array }>; export async function GetFolderCharts( folder: FolderDocument, filter: FilterQuery, getSongs: false -): Promise<{ charts: ChartDocument[] }>; +): Promise<{ charts: Array }>; export async function GetFolderCharts( folder: FolderDocument, filter: FilterQuery = {}, getSongs = false -): Promise<{ songs?: SongDocument[]; charts: ChartDocument[] }> { +): Promise<{ songs?: Array; charts: Array }> { const chartIDs = await GetFolderChartIDs(folder.folderID); const charts = await db.charts[folder.game].find( @@ -157,6 +157,7 @@ export async function GetFolderChartIDs(folderID: string) { return chartIDs.map((e) => e.chartID); } + export async function CreateFolderChartLookup(folder: FolderDocument, flush = false) { try { const { charts } = await ResolveFolderToCharts(folder, {}, false); @@ -194,6 +195,7 @@ export async function InitaliseFolderChartLookup() { const folders = await db.folders.find({ game: { $in: TachiConfig.GAMES }, }); + logger.info(`Reloading ${folders.length} folders.`); await Promise.all(folders.map((folder) => CreateFolderChartLookup(folder))); @@ -230,7 +232,7 @@ export async function GetPBsOnFolder(userID: integer, folder: FolderDocument) { return { pbs, charts, songs }; } -export function CalculateLampDistribution(pbs: PBScoreDocument[]) { +export function CalculateLampDistribution(pbs: Array) { const lampDist: Partial> = {}; for (const pb of pbs) { @@ -245,7 +247,7 @@ export function CalculateLampDistribution(pbs: PBScoreDocument[]) { return lampDist; } -export function CalculateGradeDistribution(pbs: PBScoreDocument[]) { +export function CalculateGradeDistribution(pbs: Array) { const gradeDist: Partial> = {}; for (const pb of pbs) { @@ -259,6 +261,7 @@ export function CalculateGradeDistribution(pbs: PBScoreDocument[]) { return gradeDist; } + export async function GetGradeLampDistributionForFolder(userID: integer, folder: FolderDocument) { const pbData = await GetPBsOnFolder(userID, folder); @@ -270,12 +273,15 @@ export async function GetGradeLampDistributionForFolder(userID: integer, folder: }; } -export function GetGradeLampDistributionForFolders(userID: integer, folders: FolderDocument[]) { +export function GetGradeLampDistributionForFolders( + userID: integer, + folders: Array +) { return Promise.all(folders.map((f) => GetGradeLampDistributionForFolder(userID, f))); } export function CreateFolderID(query: Record, game: Game, playtype: Playtype) { - return `F${fjsh.hash(Object.assign({ game, playtype }, query), "SHA256")}`; + return `F${fjsh.hash({ game, playtype, ...query }, "SHA256")}`; } export async function GetRecentlyViewedFolders(userID: integer, game: Game, playtype: Playtype) { diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index bf5669231..79b18eea0 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -1,9 +1,10 @@ -import { exec } from "child_process"; -import crypto from "crypto"; import { ONE_HOUR } from "lib/constants/time"; import { TachiConfig } from "lib/setup/config"; -import { Game, GamePTConfig, GetGameConfig, integer, Playtype } from "tachi-common"; +import { GetGameConfig } from "tachi-common"; +import { exec } from "child_process"; +import crypto from "crypto"; import { URL } from "url"; +import type { Game, GamePTConfig, integer, Playtype } from "tachi-common"; // https://github.com/sindresorhus/escape-string-regexp/blob/main/index.js // the developer of this has migrated everything to Force ES6 style modules, @@ -39,7 +40,7 @@ export function MStoS(ms: number) { /** * Random From Array - Selects a random value from an array. */ -export function RFA(arr: T[]): T { +export function RFA(arr: Array): T { return arr[Math.floor(Math.random() * arr.length)]; } @@ -60,12 +61,12 @@ export function SplitAuthorizationHeader(authHeader: string) { * key to keyof T. * False if the object doesn't. */ -export function HasOwnProperty(obj: T, key: string | number | symbol): key is keyof T { +export function HasOwnProperty(obj: T, key: number | string | symbol): key is keyof T { return Object.prototype.hasOwnProperty.call(obj, key); } export function IsValidGame(str: string): str is Game { - return !!TachiConfig.GAMES.includes(str as Game); + return Boolean(TachiConfig.GAMES.includes(str as Game)); } export function IsValidPlaytype(game: Game, str: string): str is Playtype { @@ -83,12 +84,12 @@ export function IsString(val: unknown): val is string { return typeof val === "string"; } -export function DedupeArr(arr: T[]): T[] { +export function DedupeArr(arr: Array): Array { return [...new Set(arr)]; } export function StripUrl(url: string, userInput: string | null) { - if (!userInput) { + if (userInput === null || userInput === "") { return userInput; } @@ -122,22 +123,28 @@ export function DeleteUndefinedProps(record: any) { export function IsValidURL(string: string) { try { const url = new URL(string); + return url.protocol === "http:" || url.protocol === "https:"; - } catch (err) { + } catch (_err) { return false; } } export function Sleep(ms: number) { - return new Promise((resolve) => setTimeout(() => resolve(), ms)); + return new Promise((resolve) => + setTimeout(() => { + resolve(); + }, ms) + ); } export function GetTimeXHoursAgo(hours: integer) { return Date.now() - ONE_HOUR * hours; } -export function ApplyNTimes(n: integer, fn: (i: integer) => T): T[] { +export function ApplyNTimes(n: integer, fn: (i: integer) => T): Array { const arr = []; + for (let i = 0; i < n; i++) { arr.push(fn(i)); } @@ -176,15 +183,16 @@ export function asyncExec(command: string) { exec(command, (err, stdout, stderr) => { if (err) { // eslint-disable-next-line prefer-promise-reject-errors - return reject({ err, stdout, stderr }); + reject({ err, stdout, stderr }); + return; } - return resolve({ stdout, stderr }); + resolve({ stdout, stderr }); }); }); } -export function FormatBMSTables(bmsTables: { table: string; level: string }[]) { +export function FormatBMSTables(bmsTables: Array<{ table: string; level: string }>) { if (bmsTables.length === 0) { return null; } @@ -192,7 +200,7 @@ export function FormatBMSTables(bmsTables: { table: string; level: string }[]) { return bmsTables.map((e) => `${e.table}${e.level}`).join(", "); } -export function HumanisedJoinArray(arr: string[], lastJoiner = "or") { +export function HumanisedJoinArray(arr: Array, lastJoiner = "or") { return `${arr.slice(0, arr.length - 1).join(", ")} ${lastJoiner} ${arr[arr.length - 1]}`; } @@ -213,6 +221,6 @@ export function IsSupported(game: Game) { * @param right - The right side of elements; everything in here not in left will be * returned. */ -export function ArrayDiff(left: T[], right: T[]) { +export function ArrayDiff(left: Array, right: Array) { return right.filter((e) => !left.includes(e)); } diff --git a/server/src/utils/naive-csv-parser.ts b/server/src/utils/naive-csv-parser.ts index 9af9d99b5..a09fb2ab5 100644 --- a/server/src/utils/naive-csv-parser.ts +++ b/server/src/utils/naive-csv-parser.ts @@ -1,4 +1,4 @@ -import { KtLogger } from "lib/logger/logger"; +import type { KtLogger } from "lib/logger/logger"; export class CSVParseError extends Error { constructor(description: string) { @@ -42,7 +42,7 @@ export function NaiveCSVParse(csvBuffer: Buffer, logger: KtLogger) { rawHeaders.push(curStr); curStr = ""; } else { - curStr += char; + curStr = curStr + char; } } diff --git a/server/src/utils/prudence.ts b/server/src/utils/prudence.ts index c1b3a6762..ff8a6b38f 100644 --- a/server/src/utils/prudence.ts +++ b/server/src/utils/prudence.ts @@ -1,12 +1,15 @@ -import p, { PrudenceError, ValidSchemaValue } from "prudence"; +import p from "prudence"; +import type { PrudenceError, ValidSchemaValue } from "prudence"; export function FormatPrError(err: PrudenceError, foreword = "Error") { const receivedText = typeof err.userVal === "object" && err.userVal !== null ? "" - : ` | Received ${err.userVal} [${err.userVal === null ? "null" : typeof err.userVal}]`; + : ` | Received ${err.userVal.toString()} [${ + err.userVal === null ? "null" : typeof err.userVal + }]`; - return `${foreword}: ${err.keychain} | ${err.message}${receivedText}.`; + return `${foreword}: ${err.keychain ?? "null"} | ${err.message}${receivedText}.`; } export const optNull = (v: ValidSchemaValue) => p.optional(p.nullable(v)); diff --git a/server/src/utils/queries/auth.ts b/server/src/utils/queries/auth.ts index 00ab4310e..07ca5ddbb 100644 --- a/server/src/utils/queries/auth.ts +++ b/server/src/utils/queries/auth.ts @@ -1,9 +1,9 @@ import db from "external/mongo/db"; -import { KtLogger } from "lib/logger/logger"; import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error"; -import { integer } from "tachi-common"; +import type { KtLogger } from "lib/logger/logger"; +import type { integer } from "tachi-common"; -export function GetKaiAuth(userID: integer, service: "FLO" | "EAG" | "MIN") { +export function GetKaiAuth(userID: integer, service: "EAG" | "FLO" | "MIN") { return db["kai-auth-tokens"].findOne({ userID, service, @@ -12,7 +12,7 @@ export function GetKaiAuth(userID: integer, service: "FLO" | "EAG" | "MIN") { export async function GetKaiAuthGuaranteed( userID: integer, - service: "FLO" | "EAG" | "MIN", + service: "EAG" | "FLO" | "MIN", logger: KtLogger ) { const authDoc = await GetKaiAuth(userID, service); diff --git a/server/src/utils/queries/charts.ts b/server/src/utils/queries/charts.ts index 9759b8639..16af25c08 100644 --- a/server/src/utils/queries/charts.ts +++ b/server/src/utils/queries/charts.ts @@ -1,6 +1,6 @@ import db from "external/mongo/db"; -import { FilterQuery } from "mongodb"; -import { +import type { FilterQuery } from "mongodb"; +import type { ChartDocument, Difficulties, Game, @@ -26,9 +26,9 @@ export function FindChartWithPTDF< I extends IDStrings = IDStrings >(game: G, songID: integer, playtype: P, difficulty: Difficulties[I]) { return db.charts[game].findOne({ - songID: songID, - playtype: playtype, - difficulty: difficulty, + songID, + playtype, + difficulty, isPrimary: true, }); } @@ -49,9 +49,9 @@ export function FindChartWithPTDFVersion< version: GPTSupportedVersions[I] ) { return db.charts[game].findOne({ - songID: songID, - playtype: playtype, - difficulty: difficulty, + songID, + playtype, + difficulty, versions: version, }); } @@ -69,6 +69,7 @@ export function FindChartWithPTDFVersion< */ export function FindDDRChartOnSongHash( songHash: string, + // Technically both of these should be "ddr" instead of Game, but it proves hard to work with. playtype: Playtype, difficulty: Difficulties[IDStrings] @@ -190,7 +191,7 @@ export function FindSDVXChartOnInGameID( ) { const diffQuery = difficulty === "ANY_INF" - ? { $in: ["INF", "GRV", "HVN", "VVD"] as Difficulties["sdvx:Single"][] } + ? { $in: ["INF", "GRV", "HVN", "VVD"] as Array } : difficulty; return db.charts.sdvx.findOne({ @@ -202,12 +203,12 @@ export function FindSDVXChartOnInGameID( export function FindSDVXChartOnInGameIDVersion( inGameID: number, - difficulty: "NOV" | "ADV" | "EXH" | "MXM" | "ANY_INF", + difficulty: "ADV" | "ANY_INF" | "EXH" | "MXM" | "NOV", version: GPTSupportedVersions["sdvx:Single"] ) { const diffQuery = difficulty === "ANY_INF" - ? { $in: ["INF", "GRV", "HVN", "VVD"] as Difficulties["sdvx:Single"][] } + ? { $in: ["INF", "GRV", "HVN", "VVD"] as Array } : difficulty; return db.charts.sdvx.findOne({ @@ -219,12 +220,12 @@ export function FindSDVXChartOnInGameIDVersion( export function FindSDVXChartOnDFVersion( songID: integer, - difficulty: "NOV" | "ADV" | "EXH" | "MXM" | "ANY_INF", + difficulty: "ADV" | "ANY_INF" | "EXH" | "MXM" | "NOV", version: GPTSupportedVersions["sdvx:Single"] ) { const diffQuery = difficulty === "ANY_INF" - ? { $in: ["INF", "GRV", "HVN", "VVD"] as Difficulties["sdvx:Single"][] } + ? { $in: ["INF", "GRV", "HVN", "VVD"] as Array } : difficulty; return db.charts.sdvx.findOne({ @@ -255,7 +256,7 @@ export function FindChartOnSHA256Playtype(game: Game, hash: string, playtype: Pl }); } -export function FindChartOnARCID(game: "iidx" | "ddr" | "jubeat" | "sdvx", arcID: string) { +export function FindChartOnARCID(game: "ddr" | "iidx" | "jubeat" | "sdvx", arcID: string) { return db.charts[game].findOne({ "data.arcChartID": arcID, }); @@ -269,11 +270,11 @@ export function FindChartOnARCID(game: "iidx" | "ddr" | "jubeat" | "sdvx", arcID export async function FindChartsOnPopularity( game: Game, playtype: Playtype, - songIDs?: integer[], + songIDs?: Array, skip = 0, limit = 100, scoreCollection: "personal-bests" | "scores" = "personal-bests" -): Promise<(ChartDocument & { __playcount: integer })[]> { +): Promise> { const matchQuery: FilterQuery = { playtype, }; @@ -290,11 +291,13 @@ export async function FindChartsOnPopularity( // magnitude faster. // Not entirely sure why, but $lookup is incredibly inefficient, // and you should just avoid it. - const charts = (await db.charts[game].find(matchQuery)) as unknown as (ChartDocument & { - __playcount: integer; - })[]; + const charts = (await db.charts[game].find(matchQuery)) as unknown as Array< + ChartDocument & { + __playcount: integer; + } + >; - const scoreCounts = (await db[scoreCollection].aggregate([ + const scoreCounts = await db[scoreCollection].aggregate([ { $match: { chartID: { $in: charts.map((e) => e.chartID) } }, }, @@ -315,7 +318,7 @@ export async function FindChartsOnPopularity( { $limit: limit, }, - ])) as { _id: string; count: integer }[]; + ]); const scoreCountMap = new Map(); diff --git a/server/src/utils/queries/pbs.ts b/server/src/utils/queries/pbs.ts index 1c27afb31..fdc1a0bb6 100644 --- a/server/src/utils/queries/pbs.ts +++ b/server/src/utils/queries/pbs.ts @@ -1,5 +1,5 @@ import db from "external/mongo/db"; -import { PBScoreDocument } from "tachi-common"; +import type { PBScoreDocument } from "tachi-common"; export async function GetAdjacentAbove(userPB: PBScoreDocument, size = 5) { const adjAbove = (await db["personal-bests"].find( @@ -11,7 +11,7 @@ export async function GetAdjacentAbove(userPB: PBScoreDocument, size = 5) { limit: size, sort: { "rankingData.rank": -1 }, } - )) as PBScoreDocument<"usc:Controller" | "usc:Keyboard">[]; + )) as Array>; return adjAbove; } @@ -26,7 +26,7 @@ export async function GetAdjacentBelow(userPB: PBScoreDocument, size = 5) { limit: size, sort: { "rankingData.rank": 1 }, } - )) as PBScoreDocument<"usc:Controller" | "usc:Keyboard">[]; + )) as Array>; return adjAbove; } diff --git a/server/src/utils/queries/songs.ts b/server/src/utils/queries/songs.ts index 746a497a0..52798954f 100644 --- a/server/src/utils/queries/songs.ts +++ b/server/src/utils/queries/songs.ts @@ -1,9 +1,9 @@ -import db from "external/mongo/db"; -import { KtLogger } from "lib/logger/logger"; -import { InternalFailure } from "lib/score-import/framework/common/converter-failures"; -import { FindOneResult } from "monk"; -import { Game, integer, SongDocument } from "tachi-common"; import { EscapeStringRegexp } from "../misc"; +import db from "external/mongo/db"; +import { InternalFailure } from "lib/score-import/framework/common/converter-failures"; +import type { KtLogger } from "lib/logger/logger"; +import type { FindOneResult } from "monk"; +import type { Game, integer, SongDocument } from "tachi-common"; /** * Finds a song document for the given game with the given title (or alt-title). @@ -18,7 +18,7 @@ export function FindSongOnTitle(game: Game, title: string): Promise [e._id, e.count])); @@ -35,7 +35,7 @@ export async function GetClassDistribution( classDistCache.set(cacheKey, convert, ONE_HOUR); return convert; - } else { - return cache; } + + return cache; } diff --git a/server/src/utils/queries/summary.ts b/server/src/utils/queries/summary.ts index 4b9dcb62b..08c2904fe 100644 --- a/server/src/utils/queries/summary.ts +++ b/server/src/utils/queries/summary.ts @@ -1,7 +1,7 @@ import db from "external/mongo/db"; -import { integer } from "tachi-common"; import { GetGradeLampDistributionForFolders } from "utils/folder"; import { GetTimeXHoursAgo } from "utils/misc"; +import type { integer } from "tachi-common"; // Various utils related to the player summary endpoint. const REASONABLE_HOURS_AGO = 16; diff --git a/server/src/utils/req-tachi-data.ts b/server/src/utils/req-tachi-data.ts index c287486a6..e443eca7e 100644 --- a/server/src/utils/req-tachi-data.ts +++ b/server/src/utils/req-tachi-data.ts @@ -1,28 +1,28 @@ -import { SYMBOL_TachiData } from "lib/constants/tachi"; -import { Request } from "express"; -import { TachiRequestData } from "./types"; import deepmerge from "deepmerge"; +import { SYMBOL_TACHI_DATA } from "lib/constants/tachi"; import CreateLogCtx from "lib/logger/logger"; +import type { TachiRequestData } from "./types"; +import type { Request } from "express"; export function AssignToReqTachiData(req: Request, data: Partial) { - if (!req[SYMBOL_TachiData]) { - req[SYMBOL_TachiData] = data; + if (!req[SYMBOL_TACHI_DATA]) { + req[SYMBOL_TACHI_DATA] = data; } else { - req[SYMBOL_TachiData] = deepmerge(req[SYMBOL_TachiData]!, data); + req[SYMBOL_TACHI_DATA] = deepmerge(req[SYMBOL_TACHI_DATA]!, data); } } const logger = CreateLogCtx(__filename); export function GetUGPT(req: Request) { - if (!req[SYMBOL_TachiData]) { + if (!req[SYMBOL_TACHI_DATA]) { logger.error(`Tried to get UGPT from a req that doesn't have SYMBOL_TachiData.`); throw new Error("Failed to get UGPT."); } - const user = req[SYMBOL_TachiData]!.requestedUser; - const game = req[SYMBOL_TachiData]!.game; - const playtype = req[SYMBOL_TachiData]!.playtype; + const user = req[SYMBOL_TACHI_DATA]!.requestedUser; + const game = req[SYMBOL_TACHI_DATA]!.game; + const playtype = req[SYMBOL_TACHI_DATA]!.playtype; if (!user || !game || !playtype) { logger.error(`Tried to get UGPT from a req that doesnt have U, G and PT.`); @@ -33,13 +33,13 @@ export function GetUGPT(req: Request) { } export function GetGPT(req: Request) { - if (!req[SYMBOL_TachiData]) { + if (!req[SYMBOL_TACHI_DATA]) { logger.error(`Tried to get GPT from a req that doesn't have SYMBOL_TachiData.`); throw new Error("Failed to get GPT."); } - const game = req[SYMBOL_TachiData]!.game; - const playtype = req[SYMBOL_TachiData]!.playtype; + const game = req[SYMBOL_TACHI_DATA]!.game; + const playtype = req[SYMBOL_TACHI_DATA]!.playtype; if (!game || !playtype) { logger.error(`Tried to get GPT from a req that doesnt have a Game and Playtype.`); diff --git a/server/src/utils/reset-state/destroy-ugpt.ts b/server/src/utils/reset-state/destroy-ugpt.ts index ea951155e..93bb5f8cb 100644 --- a/server/src/utils/reset-state/destroy-ugpt.ts +++ b/server/src/utils/reset-state/destroy-ugpt.ts @@ -1,6 +1,7 @@ import db from "external/mongo/db"; -import { integer, Game, Playtypes, Playtype } from "tachi-common"; +import { Playtypes } from "tachi-common"; import { UpdateAllPBs } from "utils/calculations/recalc-scores"; +import type { integer, Game, Playtype } from "tachi-common"; /** * Completely resets a UGPT profile. diff --git a/server/src/utils/scores.ts b/server/src/utils/scores.ts index 9f411e3c0..55bdb065a 100644 --- a/server/src/utils/scores.ts +++ b/server/src/utils/scores.ts @@ -1,6 +1,12 @@ -import db from "external/mongo/db"; -import { ChartDocument, integer, PBScoreDocument, ScoreDocument, SongDocument } from "tachi-common"; import { DedupeArr } from "./misc"; +import db from "external/mongo/db"; +import type { + ChartDocument, + integer, + PBScoreDocument, + ScoreDocument, + SongDocument, +} from "tachi-common"; export function GetPBOnChart(userID: integer, chartID: string) { return db["personal-bests"].findOne({ @@ -17,9 +23,9 @@ export function GetServerRecordOnChart(chartID: string) { } export function FilterChartsAndSongs( - scores: (PBScoreDocument | ScoreDocument)[], - charts: ChartDocument[], - songs: SongDocument[] + scores: Array, + charts: Array, + songs: Array ) { const chartIDs = new Set(); const songIDs = new Set(); diff --git a/server/src/utils/session.ts b/server/src/utils/session.ts index 6c54a1b09..375a749d2 100644 --- a/server/src/utils/session.ts +++ b/server/src/utils/session.ts @@ -1,6 +1,6 @@ import db from "external/mongo/db"; -import { Condition } from "mongodb"; -import { ScoreDocument, SessionDocument, SessionScoreInfo } from "tachi-common"; +import type { Condition } from "mongodb"; +import type { ScoreDocument, SessionDocument, SessionScoreInfo } from "tachi-common"; /** * Returns all the score documents inside a session. diff --git a/server/src/utils/string-checks.ts b/server/src/utils/string-checks.ts index 957723400..2dc4d7313 100644 --- a/server/src/utils/string-checks.ts +++ b/server/src/utils/string-checks.ts @@ -1,5 +1,6 @@ -import { Game, GamePTConfig, GetGamePTConfig, Playtype, Playtypes } from "tachi-common"; import { IsString } from "./misc"; +import { GetGamePTConfig, Playtypes } from "tachi-common"; +import type { Game, GamePTConfig, Playtype } from "tachi-common"; const isIntegerRegex = /^-?\d+$/u; diff --git a/server/src/utils/types.ts b/server/src/utils/types.ts index d77c5e075..b8236c094 100644 --- a/server/src/utils/types.ts +++ b/server/src/utils/types.ts @@ -1,4 +1,4 @@ -import { +import type { FolderDocument, TableDocument, SessionDocument, @@ -20,8 +20,8 @@ import { ImportDocument, } from "tachi-common"; +// Inject additional properties on express-session declare module "express-session" { - // Inject additional properties on express-session interface SessionData { tachi: TachiSessionData; } diff --git a/server/src/utils/user.ts b/server/src/utils/user.ts index 0d08ef1cc..4a1f74fd6 100644 --- a/server/src/utils/user.ts +++ b/server/src/utils/user.ts @@ -1,16 +1,15 @@ import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; -import { FindOneResult } from "monk"; -import { +import { GetGamePTConfig, UserAuthLevels } from "tachi-common"; +import type { FindOneResult } from "monk"; +import type { APITokenDocument, Game, - GetGamePTConfig, IDStrings, integer, Playtype, PublicUserDocument, UGSRatingsLookup, - UserAuthLevels, UserGameStats, } from "tachi-common"; @@ -46,13 +45,13 @@ export function GetUserCaseInsensitive( ): Promise> { return db.users.findOne({ usernameLowercase: username.toLowerCase(), - }) as Promise>; + }); } export async function CheckIfEmailInUse(email: string) { const doc = await db["user-private-information"].findOne({ email }); - return !!doc; + return Boolean(doc); } export function GetUserPrivateInfo(userID: integer) { @@ -65,19 +64,19 @@ export function GetUserPrivateInfo(userID: integer) { export function GetUserWithID(userID: integer): Promise> { return db.users.findOne({ id: userID, - }) as Promise>; + }); } export function GetSettingsForUser(userID: integer) { return db["user-settings"].findOne({ - userID: userID, + userID, }); } /** * Gets the users for these user IDs. */ -export async function GetUsersWithIDs(userIDs: integer[]) { +export async function GetUsersWithIDs(userIDs: Array) { const users = await db.users.find({ id: { $in: userIDs }, }); @@ -123,7 +122,7 @@ export async function GetUserWithIDGuaranteed(userID: integer): Promise