From 489c45c4a4c20ae744978832279970bb37404bc2 Mon Sep 17 00:00:00 2001 From: zkldi Date: Wed, 11 Aug 2021 19:29:49 +0100 Subject: [PATCH] Import "API-SYNC" method Fixes #275 --- server/package.json | 2 +- server/pnpm-lock.yaml | 8 +- server/src/external/mongo/db.ts | 2 + .../import-types/api/arc-ddr/parser.test.ts | 55 +++++++++---- .../import-types/api/arc-ddr/parser.ts | 12 ++- .../import-types/api/arc-iidx/parser.test.ts | 55 +++++++++---- .../import-types/api/arc-iidx/parser.ts | 10 ++- .../import-types/api/arc-sdvx/parser.test.ts | 54 +++++++++---- .../import-types/api/arc-sdvx/parser.ts | 12 ++- .../import-types/api/eag-iidx/parser.ts | 7 +- .../import-types/api/eag-sdvx/parser.ts | 7 +- .../import-types/api/flo-iidx/parser.ts | 7 +- .../import-types/api/flo-sdvx/parser.ts | 7 +- .../import-types/api/min-sdvx/parser.ts | 7 +- .../import-types/file/batch-manual/parser.ts | 2 +- server/src/lib/setup/config.ts | 16 ++++ .../v1/games/_game/_playtype/router.test.ts | 76 +++++++++++++++++- .../api/v1/games/_game/_playtype/router.ts | 17 ---- .../router/api/v1/import/router.test.ts | 14 ++++ .../src/server/router/api/v1/import/router.ts | 79 ++++++++++++++++++- .../mock-db/arc-saved-profiles.json | 1 + server/src/utils/queries/auth.ts | 51 ++++++++++++ 22 files changed, 412 insertions(+), 89 deletions(-) create mode 100644 server/src/test-utils/mock-db/arc-saved-profiles.json create mode 100644 server/src/utils/queries/auth.ts diff --git a/server/package.json b/server/package.json index ead3f2ff4..df2fe8c0c 100644 --- a/server/package.json +++ b/server/package.json @@ -70,7 +70,7 @@ "redis": "3.1.2", "rimraf": "3.0.2", "safe-json-stringify": "1.2.0", - "tachi-common": "^0.1.6", + "tachi-common": "^0.1.9", "typescript": "4.3.4", "winston": "3.3.3" }, diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index 022bd1196..c3439b82d 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -51,7 +51,7 @@ specifiers: rimraf: 3.0.2 safe-json-stringify: 1.2.0 supertest: 6.1.3 - tachi-common: ^0.1.6 + tachi-common: ^0.1.9 tap: 15.0.9 ts-node: 10.0.0 tsconfig-paths: 3.10.1 @@ -84,7 +84,7 @@ dependencies: redis: 3.1.2 rimraf: 3.0.2 safe-json-stringify: 1.2.0 - tachi-common: 0.1.6_ts-node@10.0.0+typescript@4.3.4 + tachi-common: 0.1.9_ts-node@10.0.0+typescript@4.3.4 typescript: 4.3.4 winston: 3.3.3 @@ -3774,8 +3774,8 @@ packages: strip-ansi: 6.0.0 dev: true - /tachi-common/0.1.6_ts-node@10.0.0+typescript@4.3.4: - resolution: {integrity: sha512-H2kjAIUXyvNGuPlOgvWc25KmujETee4XqRRSG/BQIall0FSiO7yWApZQMelQZITebdlKwzpd976P2NSejTv0Yg==} + /tachi-common/0.1.9_ts-node@10.0.0+typescript@4.3.4: + resolution: {integrity: sha512-nNChWe41kwizagY9tS5PCHTTSbT7VP87lejXn8sX19x+StNax/KJDyGhwXEuYyqo0pCoPYttHGZc4Aar6SgVyQ==} dependencies: monk: 7.3.4 tap: 15.0.9_ts-node@10.0.0+typescript@4.3.4 diff --git a/server/src/external/mongo/db.ts b/server/src/external/mongo/db.ts index 775dc39f5..6fbc33814 100644 --- a/server/src/external/mongo/db.ts +++ b/server/src/external/mongo/db.ts @@ -27,6 +27,7 @@ import { ImportLockDocument, UGPTSettings, SessionViewDocument, + ARCSavedProfileDocument, } from "tachi-common"; import monk, { TMiddleware } from "monk"; import CreateLogCtx from "lib/logger/logger"; @@ -146,6 +147,7 @@ const db = { "game-settings": monkDB.get("game-settings"), "game-stats-snapshots": monkDB.get("game-stats-snapshots"), "session-view-cache": monkDB.get("session-view-cache"), + "arc-saved-profiles": monkDB.get("arc-saved-profiles"), }; export default db; diff --git a/server/src/lib/score-import/import-types/api/arc-ddr/parser.test.ts b/server/src/lib/score-import/import-types/api/arc-ddr/parser.test.ts index 9c6c09465..89a65210c 100644 --- a/server/src/lib/score-import/import-types/api/arc-ddr/parser.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-ddr/parser.test.ts @@ -5,29 +5,37 @@ import { MockJSONFetch } from "test-utils/mock-fetch"; import ResetDBState from "test-utils/resets"; import CreateLogCtx from "lib/logger/logger"; import { ParseArcDDR } from "./parser"; +import db from "external/mongo/db"; const logger = CreateLogCtx(__filename); t.test("#ParseArcDDR", (t) => { t.beforeEach(ResetDBState); + const mockArcAPI = MockJSONFetch({ + "https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile": { + _links: { + _next: "https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile&page=2", + }, + _items: [1, 2, 3], + }, + "https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile&page=2": { + _links: { + _next: null, + }, + _items: [4, 5, 6], + }, + }); + t.test("Should iterate over the API.", async (t) => { - const mockArcAPI = MockJSONFetch({ - "https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile": { - _links: { - _next: "https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile&page=2", - }, - _items: [1, 2, 3], - }, - "https://arc.example.com/api/v1/ddr/16/player_bests?profile_id=profile&page=2": { - _links: { - _next: null, - }, - _items: [4, 5, 6], - }, + await db["arc-saved-profiles"].insert({ + userID: 1, + accountID: "profile", + forImportType: "api/arc-ddr", + name: "foo", }); - const res = ParseArcDDR("profile", logger, mockArcAPI); + const res = await ParseArcDDR(1, logger, mockArcAPI); t.equal(res.game, "ddr"); t.strictSame(res.context, {}); @@ -40,6 +48,25 @@ t.test("#ParseArcDDR", (t) => { t.end(); }); + t.test("Should throw a fatal error if no saved profile exists for this user.", async (t) => { + t.rejects(() => ParseArcDDR(1, logger, mockArcAPI), { + message: /No authentication was stored for api\/arc-ddr/iu, + }); + + await db["arc-saved-profiles"].insert({ + userID: 1, + accountID: "profile", + forImportType: "api/arc-sdvx", + name: "SDVX PROFILE", + }); + + t.rejects(() => ParseArcDDR(1, logger, mockArcAPI), { + message: /No authentication was stored for api\/arc-ddr/iu, + }); + + t.end(); + }); + t.end(); }); diff --git a/server/src/lib/score-import/import-types/api/arc-ddr/parser.ts b/server/src/lib/score-import/import-types/api/arc-ddr/parser.ts index 3efa31c1c..e169a7fb7 100644 --- a/server/src/lib/score-import/import-types/api/arc-ddr/parser.ts +++ b/server/src/lib/score-import/import-types/api/arc-ddr/parser.ts @@ -4,17 +4,21 @@ import { TraverseKaiAPI } from "../../common/api-kai/traverse-api"; import { ParserFunctionReturns } from "../../common/types"; import { EmptyObject } from "utils/types"; import { ServerConfig } from "lib/setup/config"; +import { integer } from "tachi-common"; +import { GetArcAuthGuaranteed } from "utils/queries/auth"; -export function ParseArcDDR( - arcProfileID: string, +export async function ParseArcDDR( + userID: integer, logger: KtLogger, fetch = nodeFetch -): ParserFunctionReturns { +): Promise> { + const authDoc = await GetArcAuthGuaranteed(userID, "api/arc-ddr", logger); + return { iterable: TraverseKaiAPI( ServerConfig.ARC_API_URL, // DDR Ace. - `/api/v1/ddr/16/player_bests?profile_id=${arcProfileID}`, + `/api/v1/ddr/16/player_bests?profile_id=${authDoc.accountID}`, ServerConfig.ARC_AUTH_TOKEN, logger, fetch diff --git a/server/src/lib/score-import/import-types/api/arc-iidx/parser.test.ts b/server/src/lib/score-import/import-types/api/arc-iidx/parser.test.ts index f1c6d9e3d..8c3353862 100644 --- a/server/src/lib/score-import/import-types/api/arc-iidx/parser.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-iidx/parser.test.ts @@ -5,29 +5,37 @@ import { MockJSONFetch } from "test-utils/mock-fetch"; import ResetDBState from "test-utils/resets"; import CreateLogCtx from "lib/logger/logger"; import { ParseArcIIDX } from "./parser"; +import db from "external/mongo/db"; const logger = CreateLogCtx(__filename); t.test("#ParseArcIIDX", (t) => { t.beforeEach(ResetDBState); + const mockArcAPI = MockJSONFetch({ + "https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile": { + _links: { + _next: "https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile&page=2", + }, + _items: [1, 2, 3], + }, + "https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile&page=2": { + _links: { + _next: null, + }, + _items: [4, 5, 6], + }, + }); + t.test("Should iterate over the API.", async (t) => { - const mockArcAPI = MockJSONFetch({ - "https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile": { - _links: { - _next: "https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile&page=2", - }, - _items: [1, 2, 3], - }, - "https://arc.example.com/api/v1/iidx/27/player_bests?profile_id=profile&page=2": { - _links: { - _next: null, - }, - _items: [4, 5, 6], - }, + await db["arc-saved-profiles"].insert({ + userID: 1, + accountID: "profile", + forImportType: "api/arc-iidx", + name: "foo", }); - const res = await ParseArcIIDX("profile", logger, mockArcAPI); + const res = await ParseArcIIDX(1, logger, mockArcAPI); t.equal(res.game, "iidx"); t.strictSame(res.context, {}); @@ -39,6 +47,25 @@ t.test("#ParseArcIIDX", (t) => { t.end(); }); + t.test("Should throw a fatal error if no saved profile exists for this user.", async (t) => { + t.rejects(() => ParseArcIIDX(1, logger, mockArcAPI), { + message: /No authentication was stored for api\/arc-iidx/iu, + }); + + await db["arc-saved-profiles"].insert({ + userID: 1, + accountID: "profile", + forImportType: "api/arc-sdvx", + name: "SDVX PROFILE", + }); + + t.rejects(() => ParseArcIIDX(1, logger, mockArcAPI), { + message: /No authentication was stored for api\/arc-iidx/iu, + }); + + t.end(); + }); + t.end(); }); 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 0d1c1d7ac..f20400102 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 @@ -5,24 +5,28 @@ import { ParserFunctionReturns } from "../../common/types"; import { EmptyObject } from "utils/types"; import { ServerConfig } from "lib/setup/config"; import { CreateArcIIDXClassHandler } from "./class-handler"; +import { GetArcAuthGuaranteed } from "utils/queries/auth"; +import { integer } from "tachi-common"; export async function ParseArcIIDX( - arcProfileID: string, + userID: integer, logger: KtLogger, fetch = nodeFetch ): Promise> { + const authDoc = await GetArcAuthGuaranteed(userID, "api/arc-iidx", logger); + return { iterable: TraverseKaiAPI( ServerConfig.ARC_API_URL, // HEROIC VERSE - `/api/v1/iidx/27/player_bests?profile_id=${arcProfileID}`, + `/api/v1/iidx/27/player_bests?profile_id=${authDoc.accountID}`, ServerConfig.ARC_AUTH_TOKEN, logger, fetch ), context: {}, classHandler: await CreateArcIIDXClassHandler( - arcProfileID, + authDoc.accountID, ServerConfig.ARC_AUTH_TOKEN, fetch ), diff --git a/server/src/lib/score-import/import-types/api/arc-sdvx/parser.test.ts b/server/src/lib/score-import/import-types/api/arc-sdvx/parser.test.ts index 42243a390..c582b2944 100644 --- a/server/src/lib/score-import/import-types/api/arc-sdvx/parser.test.ts +++ b/server/src/lib/score-import/import-types/api/arc-sdvx/parser.test.ts @@ -5,29 +5,36 @@ import { MockJSONFetch } from "test-utils/mock-fetch"; import ResetDBState from "test-utils/resets"; import CreateLogCtx from "lib/logger/logger"; import { ParseArcSDVX } from "./parser"; +import db from "external/mongo/db"; const logger = CreateLogCtx(__filename); t.test("#ParseArcSDVX", (t) => { t.beforeEach(ResetDBState); + const mockArcAPI = MockJSONFetch({ + "https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile": { + _links: { + _next: "https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile&page=2", + }, + _items: [1, 2, 3], + }, + "https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile&page=2": { + _links: { + _next: null, + }, + _items: [4, 5, 6], + }, + }); t.test("Should iterate over the API.", async (t) => { - const mockArcAPI = MockJSONFetch({ - "https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile": { - _links: { - _next: "https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile&page=2", - }, - _items: [1, 2, 3], - }, - "https://arc.example.com/api/v1/sdvx/5/player_bests?profile_id=profile&page=2": { - _links: { - _next: null, - }, - _items: [4, 5, 6], - }, + await db["arc-saved-profiles"].insert({ + userID: 1, + accountID: "profile", + forImportType: "api/arc-sdvx", + name: "foo", }); - const res = ParseArcSDVX("profile", logger, mockArcAPI); + const res = await ParseArcSDVX(1, logger, mockArcAPI); t.equal(res.game, "sdvx"); t.strictSame(res.context, {}); @@ -40,6 +47,25 @@ t.test("#ParseArcSDVX", (t) => { t.end(); }); + t.test("Should throw a fatal error if no saved profile exists for this user.", async (t) => { + t.rejects(() => ParseArcSDVX(1, logger, mockArcAPI), { + message: /No authentication was stored for api\/arc-sdvx/iu, + }); + + await db["arc-saved-profiles"].insert({ + userID: 1, + accountID: "profile", + forImportType: "api/arc-iidx", + name: "IIDX PROFILE", + }); + + t.rejects(() => ParseArcSDVX(1, logger, mockArcAPI), { + message: /No authentication was stored for api\/arc-sdvx/iu, + }); + + t.end(); + }); + t.end(); }); 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 1bf0e6f68..40ebc0193 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 @@ -4,17 +4,21 @@ import { TraverseKaiAPI } from "../../common/api-kai/traverse-api"; import { ParserFunctionReturns } from "../../common/types"; import { EmptyObject } from "utils/types"; import { ServerConfig } from "lib/setup/config"; +import { GetArcAuthGuaranteed } from "utils/queries/auth"; +import { integer } from "tachi-common"; -export function ParseArcSDVX( - arcProfileID: string, +export async function ParseArcSDVX( + userID: integer, logger: KtLogger, fetch = nodeFetch -): ParserFunctionReturns { +): Promise> { + const authDoc = await GetArcAuthGuaranteed(userID, "api/arc-sdvx", logger); + return { iterable: TraverseKaiAPI( ServerConfig.ARC_API_URL, // VIVID WAVE. - `/api/v1/sdvx/5/player_bests?profile_id=${arcProfileID}`, + `/api/v1/sdvx/5/player_bests?profile_id=${authDoc.accountID}`, ServerConfig.ARC_AUTH_TOKEN, logger, fetch 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 0acbfc962..6dd530dcf 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,10 @@ import { KtLogger } from "lib/logger/logger"; import { ParseKaiIIDX } from "../../common/api-kai/iidx/parser"; -import { KaiAuthDocument } from "tachi-common"; +import { integer } from "tachi-common"; +import { GetKaiAuthGuaranteed } from "utils/queries/auth"; + +export async function ParseEagIIDX(userID: integer, logger: KtLogger) { + const authDoc = await GetKaiAuthGuaranteed(userID, "EAG", logger); -export function ParseEagIIDX(authDoc: KaiAuthDocument, logger: KtLogger) { return ParseKaiIIDX("EAG", authDoc, 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 e9b50f376..474f98d48 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,10 @@ import { KtLogger } from "lib/logger/logger"; -import { KaiAuthDocument } from "tachi-common"; +import { integer, KaiAuthDocument } from "tachi-common"; +import { GetKaiAuthGuaranteed } from "utils/queries/auth"; import { ParseKaiSDVX } from "../../common/api-kai/sdvx/parser"; -export function ParseEagSDVX(authDoc: KaiAuthDocument, logger: KtLogger) { +export async function ParseEagSDVX(userID: integer, logger: KtLogger) { + const authDoc = await GetKaiAuthGuaranteed(userID, "EAG", logger); + return ParseKaiSDVX("EAG", authDoc, 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 61b9fbff0..cef49e74d 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,10 @@ import { KtLogger } from "lib/logger/logger"; import { ParseKaiIIDX } from "../../common/api-kai/iidx/parser"; -import { KaiAuthDocument } from "tachi-common"; +import { integer } from "tachi-common"; +import { GetKaiAuthGuaranteed } from "utils/queries/auth"; + +export async function ParseFloIIDX(userID: integer, logger: KtLogger) { + const authDoc = await GetKaiAuthGuaranteed(userID, "FLO", logger); -export function ParseFloIIDX(authDoc: KaiAuthDocument, logger: KtLogger) { return ParseKaiIIDX("FLO", authDoc, 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 9346091cd..d612f8c6a 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,10 @@ import { KtLogger } from "lib/logger/logger"; -import { KaiAuthDocument } from "tachi-common"; +import { integer, KaiAuthDocument } from "tachi-common"; +import { GetKaiAuthGuaranteed } from "utils/queries/auth"; import { ParseKaiSDVX } from "../../common/api-kai/sdvx/parser"; -export function ParseFloSDVX(authDoc: KaiAuthDocument, logger: KtLogger) { +export async function ParseFloSDVX(userID: integer, logger: KtLogger) { + const authDoc = await GetKaiAuthGuaranteed(userID, "FLO", logger); + return ParseKaiSDVX("FLO", authDoc, 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 c11cc7478..253d56cc7 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,10 @@ import { KtLogger } from "lib/logger/logger"; -import { KaiAuthDocument } from "tachi-common"; +import { integer } from "tachi-common"; +import { GetKaiAuthGuaranteed } from "utils/queries/auth"; import { ParseKaiSDVX } from "../../common/api-kai/sdvx/parser"; -export function ParseMinSDVX(authDoc: KaiAuthDocument, logger: KtLogger) { +export async function ParseMinSDVX(userID: integer, logger: KtLogger) { + const authDoc = await GetKaiAuthGuaranteed(userID, "MIN", logger); + return ParseKaiSDVX("MIN", authDoc, logger); } 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 7f4835fba..e244ec00f 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 @@ -21,7 +21,7 @@ function ParseBatchManual( } catch (err) { throw new ScoreImportFatalError( 400, - `Invalid JSON. (${err?.message ?? "No Error Message Available."})` + `Invalid JSON. (${(err as Error)?.message ?? "No Error Message Available."})` ); } diff --git a/server/src/lib/setup/config.ts b/server/src/lib/setup/config.ts index be1730def..3224106e7 100644 --- a/server/src/lib/setup/config.ts +++ b/server/src/lib/setup/config.ts @@ -37,6 +37,11 @@ function isValidURL(self: unknown) { } } +interface OAuth2Info { + CLIENT_ID: string; + CLIENT_SECRET: string; +} + export interface TachiConfig { MONGO_CONNECTION_URL: string; MONGO_DATABASE_NAME: string; @@ -47,6 +52,9 @@ export interface TachiConfig { EAG_API_URL: string; MIN_API_URL: string; ARC_API_URL: string; + FLO_OAUTH2_INFO?: OAuth2Info; + EAG_OAUTH2_INFO?: OAuth2Info; + MIN_OAUTH2_INFO?: OAuth2Info; ARC_AUTH_TOKEN: string; CDN_FILE_ROOT: string; TYPE: "ktchi" | "btchi" | "omni"; @@ -57,6 +65,11 @@ export interface TachiConfig { SERVER_TYPE_INFO: StaticConfig.ServerConfig; } +const isValidOauth2 = p.optional({ + CLIENT_ID: "string", + CLIENT_SECRET: "string", +}); + const err = p(config, { MONGO_CONNECTION_URL: "string", MONGO_DATABASE_NAME: "string", @@ -67,6 +80,9 @@ const err = p(config, { EAG_API_URL: isValidURL, MIN_API_URL: isValidURL, ARC_API_URL: isValidURL, + FLO_OAUTH2_INFO: isValidOauth2, + EAG_OAUTH2_INFO: isValidOauth2, + MIN_OAUTH2_INFO: isValidOauth2, ARC_AUTH_TOKEN: "string", CDN_FILE_ROOT: "string", PORT: p.isPositiveInteger, diff --git a/server/src/server/router/api/v1/games/_game/_playtype/router.test.ts b/server/src/server/router/api/v1/games/_game/_playtype/router.test.ts index a9d086f7c..f85c996c6 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/router.test.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/router.test.ts @@ -1,7 +1,9 @@ import t from "tap"; import mockApi from "test-utils/mock-api"; -import { GetGamePTConfig } from "tachi-common"; +import { GetGamePTConfig, UserGameStats } from "tachi-common"; import { CloseAllConnections } from "test-utils/close-connections"; +import ResetDBState from "test-utils/resets"; +import db from "external/mongo/db"; t.test("GET /api/v1/games/:game/:playtype", (t) => { t.test("Should return information about the game:playtype.", async (t) => { @@ -25,4 +27,76 @@ t.test("GET /api/v1/games/:game/:playtype", (t) => { t.end(); }); +t.test("GET /api/v1/games/:game/:playtype/leaderboard", (t) => { + t.beforeEach(ResetDBState); + + t.test("Should return the leaderboards for this game", async (t) => { + const res = await mockApi.get("/api/v1/games/iidx/SP/leaderboard"); + + t.equal(res.statusCode, 200); + + t.equal(res.body.body.gameStats.length, 1); + t.equal(res.body.body.users.length, 1); + + t.hasStrict(res.body.body, { + gameStats: [ + { + userID: 1, + game: "iidx", + playtype: "SP", + }, + ], + users: [ + { + id: 1, + }, + ], + }); + + t.end(); + }); + + t.test("Should reject unknown alg", async (t) => { + const res = await mockApi.get("/api/v1/games/iidx/SP/leaderboard?alg=naiveRating"); + + t.equal(res.statusCode, 400); + + t.end(); + }); + + t.test("Should use provided algorithm to resort results.", async (t) => { + await db["game-stats"].insert([ + { + userID: 2, + game: "iidx", + playtype: "SP", + ratings: { + BPI: 100, + ktRating: 50, + }, + }, + { + userID: 3, + game: "iidx", + playtype: "SP", + ratings: { + BPI: 50, + ktRating: 100, + }, + }, + ] as UserGameStats[]); + + const res = await mockApi.get("/api/v1/games/iidx/SP/leaderboard?alg=BPI"); + + t.strictSame( + res.body.body.gameStats.map((e: UserGameStats) => e.userID), + [2, 3, 1] + ); + + t.end(); + }); + + t.end(); +}); + t.teardown(CloseAllConnections); 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 caac0b3b8..ba635ca4f 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 @@ -37,7 +37,6 @@ router.get("/", (req, res) => { * Returns user-game-stats for this game in batches of 100. * This is sorted by the games default-sorting-statistic. * - * @param start - A number dictating what value to start from. * @param alg - An alternative algorithm to use instead of the gpts default. * * @name GET /api/v1/games/:game/:playtype/leaderboard @@ -47,18 +46,6 @@ router.get("/leaderboard", async (req, res) => { const playtype = req[SYMBOL_TachiData]!.playtype!; const gptConfig = GetGamePTConfig(game, playtype); - let start: null | integer = null; - if (IsString(req.query.start)) { - start = ParseStrPositiveInt(req.query.start); - - if (start === null) { - return res.status(400).json({ - success: false, - description: `Invalid value of ${req.query.start} for start.`, - }); - } - } - let alg = gptConfig.defaultProfileRatingAlg; if (IsString(req.query.alg)) { const temp = CheckStrProfileAlg(game, playtype, req.query.alg); @@ -81,10 +68,6 @@ router.get("/leaderboard", async (req, res) => { }, }; - if (start !== null) { - options.skip = start * 100; - } - const gameStats = await db["game-stats"].find( { game, diff --git a/server/src/server/router/api/v1/import/router.test.ts b/server/src/server/router/api/v1/import/router.test.ts index 563a72bfc..1082097e5 100644 --- a/server/src/server/router/api/v1/import/router.test.ts +++ b/server/src/server/router/api/v1/import/router.test.ts @@ -225,6 +225,20 @@ t.test("POST /api/v1/import/file", async (t) => { t.end(); }); + t.test("Invalid JSON", async (t) => { + const res = await mockApi + .post("/api/v1/import/file") + .set("Cookie", cookie) + .attach("scoreData", Buffer.from("{invalid JSON")) + .field("importType", "file/batch-manual"); + + t.equal(res.body.success, false, "Should be successful."); + + t.equal(res.statusCode, 400); + + t.end(); + }); + t.test("Single import", async (t) => { const res = await mockApi .post("/api/v1/import/file") diff --git a/server/src/server/router/api/v1/import/router.ts b/server/src/server/router/api/v1/import/router.ts index 558384cf4..ca2445c26 100644 --- a/server/src/server/router/api/v1/import/router.ts +++ b/server/src/server/router/api/v1/import/router.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import { FileUploadImportTypes } from "tachi-common"; +import { APIImportTypes, FileUploadImportTypes, integer } from "tachi-common"; import Prudence from "prudence"; import { GetUserWithIDGuaranteed } from "utils/user"; import CreateLogCtx, { KtLogger } from "lib/logger/logger"; @@ -16,6 +16,14 @@ import { ParseMerIIDX } from "lib/score-import/import-types/file/mer-iidx/parser import ParsePLIIIDXCSV from "lib/score-import/import-types/file/pli-iidx-csv/parser"; import { ServerTypeInfo } from "lib/setup/config"; import { RequirePermissions } from "server/middleware/auth"; +import { ParseEagIIDX } from "lib/score-import/import-types/api/eag-iidx/parser"; +import { ParseEagSDVX } from "lib/score-import/import-types/api/eag-sdvx/parser"; +import { ParseFloIIDX } from "lib/score-import/import-types/api/flo-iidx/parser"; +import { ParseFloSDVX } from "lib/score-import/import-types/api/flo-sdvx/parser"; +import { ParseMinSDVX } from "lib/score-import/import-types/api/min-sdvx/parser"; +import { ParseArcDDR } from "lib/score-import/import-types/api/arc-ddr/parser"; +import { ParseArcSDVX } from "lib/score-import/import-types/api/arc-sdvx/parser"; +import { ParseArcIIDX } from "lib/score-import/import-types/api/arc-iidx/parser"; const logger = CreateLogCtx(__filename); @@ -27,6 +35,8 @@ const ParseMultipartScoredata = CreateMulterSingleUploadMiddleware( logger ); +const fileImportTypes = ServerTypeInfo.supportedImportTypes.filter((e) => e.startsWith("file/")); + /** * Import scores from a file. Expects the post request to be multipart, and to provide a scoreData file. * @name POST /api/v1/import/file @@ -37,7 +47,7 @@ router.post( ParseMultipartScoredata, prValidate( { - importType: Prudence.isIn(ServerTypeInfo.supportedImportTypes), + importType: Prudence.isIn(fileImportTypes), }, {}, { allowExcessKeys: true } @@ -74,6 +84,43 @@ router.post( } ); +const apiImportTypes = ServerTypeInfo.supportedImportTypes.filter((e) => e.startsWith("api/")); + +/** + * Import scores from another API. This typically will perform a full sync. + * @name POST /api/v1/import/from-api + */ +router.post( + "/from-api", + RequirePermissions("submit_score"), + prValidate( + { + importType: Prudence.isIn(apiImportTypes), + }, + {}, + { allowExcessKeys: true } + ), + async (req, res) => { + const importType = req.body.importType as APIImportTypes; + + const userDoc = await GetUserWithIDGuaranteed(req.session.tachi!.userID); + + const inputParser = (logger: KtLogger) => + ResolveAPIImportParser(userDoc.id, importType, logger); + + // see the argument above about typescript falsely expanding types. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const responseData = await ExpressWrappedScoreImportMain( + userDoc, + true, + importType, + inputParser + ); + + return res.status(responseData.statusCode).json(responseData.body); + } +); + /** * Resolves the data from a file upload into an iterable, * The appropriate processing function to map that iterable over, @@ -109,4 +156,32 @@ export function ResolveFileUploadData( } } +export function ResolveAPIImportParser( + userID: integer, + importType: APIImportTypes, + logger: KtLogger +) { + switch (importType) { + case "api/eag-iidx": + return ParseEagIIDX(userID, logger); + case "api/eag-sdvx": + return ParseEagSDVX(userID, logger); + case "api/flo-iidx": + return ParseFloIIDX(userID, logger); + case "api/flo-sdvx": + return ParseFloSDVX(userID, logger); + case "api/min-sdvx": + return ParseMinSDVX(userID, logger); + case "api/arc-ddr": + return ParseArcDDR(userID, logger); + case "api/arc-iidx": + return ParseArcIIDX(userID, logger); + case "api/arc-sdvx": + return ParseArcSDVX(userID, logger); + default: + logger.error(`Unknown importType ${importType} has no handler?`); + throw new ScoreImportFatalError(500, `Unknown importType ${importType}.`); + } +} + export default router; diff --git a/server/src/test-utils/mock-db/arc-saved-profiles.json b/server/src/test-utils/mock-db/arc-saved-profiles.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/server/src/test-utils/mock-db/arc-saved-profiles.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/server/src/utils/queries/auth.ts b/server/src/utils/queries/auth.ts new file mode 100644 index 000000000..f47d40d50 --- /dev/null +++ b/server/src/utils/queries/auth.ts @@ -0,0 +1,51 @@ +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"; + +export function GetKaiAuth(userID: integer, service: "FLO" | "EAG" | "MIN") { + return db["kai-auth-tokens"].findOne({ + userID, + service, + }); +} + +export async function GetKaiAuthGuaranteed( + userID: integer, + service: "FLO" | "EAG" | "MIN", + logger: KtLogger +) { + const authDoc = await GetKaiAuth(userID, service); + + if (!authDoc) { + logger.error(`No authentication was stored for ${service}.`); + throw new ScoreImportFatalError(401, `No authentication was stored for ${service}.`); + } + + return authDoc; +} + +export function GetArcAuth( + userID: integer, + forImportType: "api/arc-iidx" | "api/arc-ddr" | "api/arc-sdvx" +) { + return db["arc-saved-profiles"].findOne({ + userID, + forImportType, + }); +} + +export async function GetArcAuthGuaranteed( + userID: integer, + forImportType: "api/arc-iidx" | "api/arc-ddr" | "api/arc-sdvx", + logger: KtLogger +) { + const authDoc = await GetArcAuth(userID, forImportType); + + if (!authDoc) { + logger.error(`No authentication was stored for ${forImportType}.`); + throw new ScoreImportFatalError(401, `No authentication was stored for ${forImportType}.`); + } + + return authDoc; +}