diff --git a/server/src/server/router/api/v1/users/_userID/router.test.ts b/server/src/server/router/api/v1/users/_userID/router.test.ts index 4a1bf3ea2..8e3489458 100644 --- a/server/src/server/router/api/v1/users/_userID/router.test.ts +++ b/server/src/server/router/api/v1/users/_userID/router.test.ts @@ -1,9 +1,11 @@ import db from "external/mongo/db"; -import { UserGameStats } from "tachi-common"; +import { ONE_DAY, ONE_YEAR } from "lib/constants/time"; +import { UserGameStats, ImportTypes, ImportDocument, Game } from "tachi-common"; import t from "tap"; import { CreateFakeAuthCookie } from "test-utils/fake-auth"; import mockApi from "test-utils/mock-api"; import ResetDBState from "test-utils/resets"; +import { Random20Hex } from "utils/misc"; import { PasswordCompare } from "../../auth/auth"; t.test("GET /api/v1/users/:userID", (t) => { @@ -368,3 +370,90 @@ t.test("GET /api/v1/users/:userID/game-stats", (t) => { t.end(); }); + +t.test("GET /api/v1/users/:userID/recent-imports", async (t) => { + t.beforeEach(ResetDBState); + + function mkImport( + game: Game, + importType: ImportTypes, + timeFinished: number, + userIntent: boolean + ): ImportDocument { + return { + classDeltas: [], + createdSessions: [], + errors: [], + game, + importType, + importID: Random20Hex(), + goalInfo: [], + idStrings: [], + milestoneInfo: [], + playtypes: [], + scoreIDs: [], + timeFinished, + timeStarted: 0, + userID: 1, + userIntent, + }; + } + + t.test("Should work cross game and ignore imports without userIntent.", async (t) => { + await db.imports.insert([ + mkImport("iidx", "api/arc-iidx", Date.now() - ONE_DAY, true), + mkImport("iidx", "api/arc-iidx", Date.now() - ONE_DAY, true), + mkImport("sdvx", "api/arc-sdvx", Date.now() - ONE_DAY, false), + mkImport("iidx", "api/arc-iidx", Date.now() - ONE_DAY, false), + mkImport("sdvx", "api/arc-sdvx", Date.now() - ONE_DAY, true), + ]); + + const res = await mockApi.get("/api/v1/users/1/recent-imports"); + + t.equal(res.statusCode, 200); + + t.strictSame(res.body.body, [ + { + importType: "api/arc-iidx", + count: 2, + }, + { + importType: "api/arc-sdvx", + count: 1, + }, + ]); + + t.end(); + }); + + t.test("Should ignore imports that were too long ago.", async (t) => { + await db.imports.insert([ + mkImport("iidx", "api/arc-iidx", Date.now() - ONE_DAY, true), + mkImport("iidx", "ir/fervidex", Date.now() - ONE_YEAR, true), + mkImport("iidx", "ir/fervidex", Date.now() - ONE_YEAR, true), + mkImport("iidx", "ir/fervidex", Date.now() - ONE_YEAR, true), + mkImport("iidx", "ir/fervidex", Date.now() - ONE_YEAR, true), + mkImport("sdvx", "api/arc-sdvx", Date.now() - ONE_DAY, true), + mkImport("sdvx", "api/arc-sdvx", Date.now() - ONE_DAY, true), + ]); + + const res = await mockApi.get("/api/v1/users/1/recent-imports"); + + t.equal(res.statusCode, 200); + + t.strictSame(res.body.body, [ + { + importType: "api/arc-sdvx", + count: 2, + }, + { + importType: "api/arc-iidx", + count: 1, + }, + ]); + + t.end(); + }); + + t.end(); +}); 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 3d28c8549..39c7b0496 100644 --- a/server/src/server/router/api/v1/users/_userID/router.ts +++ b/server/src/server/router/api/v1/users/_userID/router.ts @@ -1,12 +1,13 @@ import { Router } from "express"; import db from "external/mongo/db"; import { SYMBOL_TachiData } 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 { UserGameStats } from "tachi-common"; +import { ImportTypes, integer, UserGameStats } from "tachi-common"; import { DeleteUndefinedProps, StripUrl } from "utils/misc"; -import { optNull, optNullFluffStrField } from "utils/prudence"; +import { optNullFluffStrField } from "utils/prudence"; import { GetRecentlyViewedFoldersAnyGPT, GetRecentPlaycount, @@ -341,6 +342,38 @@ 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 recentImports = (await db.imports.aggregate([ + { + $match: { + userID: user.id, + timeFinished: { $gt: Date.now() - ONE_MONTH }, + userIntent: true, + }, + }, + { + $group: { + _id: "$importType", + count: { $sum: 1 }, + }, + }, + ])) as { _id: ImportTypes; count: integer }[]; + + // rename _id to importType. + const imports = recentImports.map((e) => ({ importType: e._id, count: e.count })); + + return res.status(200).json({ + success: true, + description: `Found ${recentImports.length} imports.`, + body: imports.sort((a, b) => b.count - a.count), + }); +}); + router.use("/games/:game/:playtype", gamePTRouter); router.use("/pfp", pfpRouter); router.use("/banner", bannerRouter);