mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-06 13:58:05 +03:00
Define new score and session routes.
This commit is contained in:
Generated
+4
-4
@@ -80,7 +80,7 @@ dependencies:
|
||||
rate-limit-redis: 2.1.0
|
||||
redis: 3.1.2
|
||||
rimraf: 3.0.2
|
||||
tachi-common: github.com/zkldi/tachi-common/0b3e388dd5f153e18a4cede1c9ba046f45c7a570_ts-node@10.0.0+typescript@4.3.4
|
||||
tachi-common: github.com/zkldi/tachi-common/478605bdec986f10af3af32be3bf7716ecec6d63_ts-node@10.0.0+typescript@4.3.4
|
||||
typescript: 4.3.4
|
||||
winston: 3.3.3
|
||||
|
||||
@@ -4233,9 +4233,9 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
github.com/zkldi/tachi-common/0b3e388dd5f153e18a4cede1c9ba046f45c7a570_ts-node@10.0.0+typescript@4.3.4:
|
||||
resolution: {tarball: https://codeload.github.com/zkldi/tachi-common/tar.gz/0b3e388dd5f153e18a4cede1c9ba046f45c7a570}
|
||||
id: github.com/zkldi/tachi-common/0b3e388dd5f153e18a4cede1c9ba046f45c7a570
|
||||
github.com/zkldi/tachi-common/478605bdec986f10af3af32be3bf7716ecec6d63_ts-node@10.0.0+typescript@4.3.4:
|
||||
resolution: {tarball: https://codeload.github.com/zkldi/tachi-common/tar.gz/478605bdec986f10af3af32be3bf7716ecec6d63}
|
||||
id: github.com/zkldi/tachi-common/478605bdec986f10af3af32be3bf7716ecec6d63
|
||||
name: tachi-common
|
||||
version: 0.1.0
|
||||
dependencies:
|
||||
|
||||
Vendored
+1
-1
@@ -62,7 +62,7 @@ monkDB
|
||||
});
|
||||
|
||||
const RemoveIDMiddleware: TMiddleware =
|
||||
({ collection, monkInstance }) =>
|
||||
() =>
|
||||
(next) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(args: any, method) => {
|
||||
|
||||
@@ -17,6 +17,9 @@ const GuestToken: APITokenDocument = {
|
||||
export const AllPermissions: Record<APIPermissions, true> = {
|
||||
customise_profile: true,
|
||||
submit_score: true,
|
||||
customise_session: true,
|
||||
customise_score: true,
|
||||
delete_score: true,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -31,7 +34,7 @@ export const AllPermissions: Record<APIPermissions, true> = {
|
||||
* If this request was made with no auth headers or session tokens, then a guest
|
||||
* token is set as the request token, with no permissions.
|
||||
*
|
||||
* This is set on req[SYMBOL_TachiAPIData].
|
||||
* This is set on req[SYMBOL_TachiAPIAuth].
|
||||
*/
|
||||
export const SetRequestPermissions: RequestHandler = async (req, res, next) => {
|
||||
if (req.session?.tachi?.userID) {
|
||||
@@ -98,7 +101,9 @@ export const RequirePermissions =
|
||||
(...perms: APIPermissions[]): RequestHandler =>
|
||||
(req, res, next) => {
|
||||
if (!req[SYMBOL_TachiAPIAuth]) {
|
||||
logger.error(`RequirePermissions middleware was hit without any TachiAPIData?`);
|
||||
logger.error(
|
||||
`RequirePermissions middleware was hit without any TachiAPIAuthentication?`
|
||||
);
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Router } from "express";
|
||||
import db from "../../../../../../../../../external/mongo/db";
|
||||
import { SYMBOL_TachiData } from "../../../../../../../../../lib/constants/tachi";
|
||||
import CreateLogCtx from "../../../../../../../../../lib/logger/logger";
|
||||
import { FormatChart } from "../../../../../../../../../utils/misc";
|
||||
import { SearchUsersRegExp } from "../../../../../../../../../lib/search/search";
|
||||
import { FormatChart, IsString } from "../../../../../../../../../utils/misc";
|
||||
import { ParseStrPositiveNonZeroInt } from "../../../../../../../../../utils/string-checks";
|
||||
import { GetUsersWithIDs } from "../../../../../../../../../utils/user";
|
||||
import { ValidateAndGetChart } from "./middleware";
|
||||
@@ -104,4 +105,38 @@ router.get("/pbs", async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Searches the PBs on this chart for the given user(s).
|
||||
*
|
||||
* @param search - The user to search for
|
||||
*
|
||||
* @name GET /api/v1/games/:game/:playtype/charts/:chartID/pbs/search
|
||||
*/
|
||||
router.get("/pbs/search", async (req, res) => {
|
||||
const chart = req[SYMBOL_TachiData]!.chartDoc!;
|
||||
|
||||
if (!IsString(req.query.search)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid parameter for search.`,
|
||||
});
|
||||
}
|
||||
|
||||
const users = await SearchUsersRegExp(req.query.search);
|
||||
|
||||
const pbs = await db["personal-bests"].find({
|
||||
chartID: chart.chartID,
|
||||
userID: { $in: users.map((e) => e.id) },
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Returned ${pbs.length} scores.`,
|
||||
body: {
|
||||
pbs,
|
||||
users,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RequestHandler } from "express";
|
||||
import db from "../../../../../../external/mongo/db";
|
||||
import { SYMBOL_TachiAPIAuth, SYMBOL_TachiData } from "../../../../../../lib/constants/tachi";
|
||||
import { AssignToReqTachiData } from "../../../../../../utils/req-tachi-data";
|
||||
|
||||
export const GetScoreFromParam: RequestHandler = async (req, res, next) => {
|
||||
@@ -20,4 +21,20 @@ export const GetScoreFromParam: RequestHandler = async (req, res, next) => {
|
||||
}
|
||||
|
||||
AssignToReqTachiData(req, { scoreDoc: score });
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
export const RequireOwnershipOfScore: RequestHandler = (req, res, next) => {
|
||||
const score = req[SYMBOL_TachiData]!.scoreDoc!;
|
||||
const userID = req[SYMBOL_TachiAPIAuth].userID!;
|
||||
|
||||
if (score.userID !== userID) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
description: `You are not authorised to perform this action.`,
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
@@ -3,7 +3,10 @@ import db from "../../../../../../external/mongo/db";
|
||||
import { SYMBOL_TachiData } from "../../../../../../lib/constants/tachi";
|
||||
import CreateLogCtx from "../../../../../../lib/logger/logger";
|
||||
import { GetUserWithID } from "../../../../../../utils/user";
|
||||
import { GetScoreFromParam } from "./middleware";
|
||||
import { RequirePermissions } from "../../../../../middleware/auth";
|
||||
import prValidate from "../../../../../middleware/prudence-validate";
|
||||
import { GetScoreFromParam, RequireOwnershipOfScore } from "./middleware";
|
||||
import p from "prudence";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
@@ -62,4 +65,57 @@ router.get("/", async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
interface ModifiableScoreProps {
|
||||
comment?: string | null;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies a score.
|
||||
*
|
||||
* Requires you to be the owner of this score, and have the modify_scores permission.
|
||||
*
|
||||
* @name PATCH /api/v1/scores/:scoreID
|
||||
*/
|
||||
router.patch(
|
||||
"/",
|
||||
RequireOwnershipOfScore,
|
||||
RequirePermissions("customise_score"),
|
||||
prValidate({
|
||||
comment: p.optional(p.nullable(p.isBoundedString(1, 140))),
|
||||
highlight: "*boolean",
|
||||
}),
|
||||
async (req, res) => {
|
||||
const score = req[SYMBOL_TachiData]!.scoreDoc!;
|
||||
|
||||
const modifyOption: ModifiableScoreProps = {};
|
||||
|
||||
if (req.body.comment !== undefined) {
|
||||
modifyOption.comment = req.body.comment;
|
||||
}
|
||||
|
||||
if (req.body.comment !== undefined) {
|
||||
modifyOption.highlight = req.body.highlight;
|
||||
}
|
||||
|
||||
if (Object.keys(modifyOption).length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `This request modifies nothing about the score.`,
|
||||
});
|
||||
}
|
||||
|
||||
const newScore = await db.scores.findOneAndUpdate(
|
||||
{ scoreID: score.scoreID },
|
||||
{ $set: modifyOption }
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Updated score.`,
|
||||
body: newScore,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -4,11 +4,11 @@ import scoreIDRouter from "./_scoreID/router";
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
/**
|
||||
* No idea yet.
|
||||
* Nothing? Maybe we can think of a good use for this endpoint at some point.
|
||||
*
|
||||
* @name GET /api/v1/scores
|
||||
* @name GET /api/v1/scores/:scoreID
|
||||
*/
|
||||
router.get("/", async (req, res) => {});
|
||||
// router.get("/", async (req, res) => {});
|
||||
|
||||
router.use("/:scoreID", scoreIDRouter);
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { RequestHandler } from "express";
|
||||
import db from "../../../../../../external/mongo/db";
|
||||
import { SYMBOL_TachiAPIAuth, SYMBOL_TachiData } from "../../../../../../lib/constants/tachi";
|
||||
import { AssignToReqTachiData } from "../../../../../../utils/req-tachi-data";
|
||||
|
||||
export const GetSessionFromParam: RequestHandler = async (req, res, next) => {
|
||||
const session = await db.sessions.findOne({
|
||||
sessionID: req.params.sessionID,
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
description: `This session does not exist.`,
|
||||
});
|
||||
}
|
||||
|
||||
AssignToReqTachiData(req, { sessionDoc: session });
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
export const RequireOwnershipOfSession: RequestHandler = (req, res, next) => {
|
||||
const userID = req[SYMBOL_TachiAPIAuth].userID;
|
||||
const session = req[SYMBOL_TachiData]!.sessionDoc!;
|
||||
|
||||
if (userID !== session.userID) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
description: `You are not authorised to modify this session.`,
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Router } from "express";
|
||||
import db from "../../../../../../external/mongo/db";
|
||||
import { SYMBOL_TachiData } from "../../../../../../lib/constants/tachi";
|
||||
import { RequirePermissions } from "../../../../../middleware/auth";
|
||||
import prValidate from "../../../../../middleware/prudence-validate";
|
||||
import p from "prudence";
|
||||
import { RequireOwnershipOfSession } from "./middleware";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
/**
|
||||
* Retrieves the session, its scores and the related songs and charts.
|
||||
*
|
||||
* @name GET /api/v1/sessions/:sessionID
|
||||
*/
|
||||
router.get("/", async (req, res) => {
|
||||
const session = req[SYMBOL_TachiData]!.sessionDoc!;
|
||||
|
||||
const scores = await db.scores.find({
|
||||
scoreID: { $in: session.scoreInfo.map((e) => e.scoreID) },
|
||||
});
|
||||
|
||||
const [songs, charts] = await Promise.all([
|
||||
db.songs[session.game].find({
|
||||
id: { $in: scores.map((e) => e.songID) },
|
||||
}),
|
||||
db.charts[session.game].find({
|
||||
chartID: { $in: scores.map((e) => e.chartID) },
|
||||
}),
|
||||
]);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Successfully returned session ${session.name}.`,
|
||||
body: {
|
||||
session,
|
||||
songs,
|
||||
charts,
|
||||
scores,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
interface ModifiableSessionProps {
|
||||
name?: string;
|
||||
desc?: string;
|
||||
highlight?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies a session.
|
||||
*
|
||||
* Requires the requester to be the owner of the session, alongside having the
|
||||
* customise_session permission.
|
||||
*
|
||||
* @param name - A new name for the session.
|
||||
* @param desc - A new desc for the session.
|
||||
* @param highlight - Update the highlighted state of the session with this.
|
||||
*
|
||||
* @name PATCH /api/v1/sessions/:sessionID
|
||||
*/
|
||||
router.patch(
|
||||
"/",
|
||||
RequireOwnershipOfSession,
|
||||
RequirePermissions("customise_session"),
|
||||
prValidate(
|
||||
{
|
||||
name: p.optional(p.isBoundedString(3, 80)),
|
||||
desc: p.optional(p.isBoundedString(3, 120)),
|
||||
highlight: "*boolean",
|
||||
},
|
||||
{},
|
||||
{ allowExcessKeys: true }
|
||||
),
|
||||
async (req, res) => {
|
||||
const session = req[SYMBOL_TachiData]!.sessionDoc!;
|
||||
|
||||
const updateExp: ModifiableSessionProps = {};
|
||||
|
||||
if (req.body.name) {
|
||||
updateExp.name = req.body.name as string;
|
||||
}
|
||||
|
||||
if (req.query.desc) {
|
||||
updateExp.desc = req.body.desc as string;
|
||||
}
|
||||
|
||||
if (typeof req.body.highlight === "boolean") {
|
||||
updateExp.highlight = req.body.highlight as boolean;
|
||||
}
|
||||
|
||||
if (Object.keys(updateExp).length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `This request modifies nothing about this session.`,
|
||||
});
|
||||
}
|
||||
|
||||
const newSession = await db.sessions.findOneAndUpdate(
|
||||
{ sessionID: session.sessionID },
|
||||
{ $set: updateExp }
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Updated Session.`,
|
||||
body: newSession,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Router } from "express";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
/**
|
||||
* nothing, yet.
|
||||
*
|
||||
* @name GET /api/v1/sessions
|
||||
*/
|
||||
// router.get("/", async (req, res) => {});
|
||||
|
||||
export default router;
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import t from "tap";
|
||||
import db from "../../../../../../../../../../external/mongo/db";
|
||||
import { CloseAllConnections } from "../../../../../../../../../../test-utils/close-connections";
|
||||
import mockApi from "../../../../../../../../../../test-utils/mock-api";
|
||||
import ResetDBState from "../../../../../../../../../../test-utils/resets";
|
||||
import {
|
||||
Testing511Song,
|
||||
LoadKTBlackIIDXData,
|
||||
GetKTDataJSON,
|
||||
} from "../../../../../../../../../../test-utils/test-data";
|
||||
import { PBScoreDocument } from "tachi-common";
|
||||
|
||||
t.test("GET /api/v1/users/:userID/games/:game/:playtype/pbs/best", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should return a users best 100 personal bests.", async (t) => {
|
||||
const mockPBs: PBScoreDocument[] = [];
|
||||
|
||||
for (let i = 0; i < 200; i++) {
|
||||
mockPBs.push({
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
isPrimary: true,
|
||||
chartID: i.toString(), // hack to generate some random chartIDs
|
||||
songID: Testing511Song.id,
|
||||
calculatedData: {
|
||||
ktRating: i,
|
||||
},
|
||||
} as PBScoreDocument);
|
||||
}
|
||||
|
||||
await db["personal-bests"].insert(mockPBs);
|
||||
|
||||
for (const sc of mockPBs) {
|
||||
delete sc._id; // lol
|
||||
}
|
||||
|
||||
const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/pbs/best");
|
||||
|
||||
t.hasStrict(res.body, {
|
||||
success: true,
|
||||
description: "Retrieved 100 personal bests.",
|
||||
body: {
|
||||
scores: mockPBs.slice(100).reverse(),
|
||||
songs: [Testing511Song],
|
||||
charts: [],
|
||||
},
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("GET /api/v1/users/:userID/games/:game/:playtype/pbs", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(LoadKTBlackIIDXData);
|
||||
|
||||
t.test("Should return 400 if no search param is given", async (t) => {
|
||||
const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/pbs");
|
||||
|
||||
t.equal(res.statusCode, 400);
|
||||
t.equal(res.body.success, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return 400 if invalid search param is given", async (t) => {
|
||||
const res = await mockApi.get(
|
||||
"/api/v1/users/test_zkldi/games/iidx/SP/pbs?search=foo&search=bar"
|
||||
);
|
||||
|
||||
t.equal(res.statusCode, 400);
|
||||
t.equal(res.body.success, false);
|
||||
|
||||
const res2 = await mockApi.get(
|
||||
"/api/v1/users/test_zkldi/games/iidx/SP/pbs?search[$where]=process.exit(1)"
|
||||
);
|
||||
|
||||
t.equal(res2.statusCode, 400);
|
||||
t.equal(res2.body.success, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should search a user's personal bests.", async (t) => {
|
||||
const mockPBs: PBScoreDocument[] = [];
|
||||
|
||||
const charts = GetKTDataJSON("./tachi/ktblack-charts-iidx.json");
|
||||
|
||||
for (let i = 0; i < 200; i++) {
|
||||
mockPBs.push({
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
isPrimary: true,
|
||||
chartID: charts[i].chartID,
|
||||
songID: charts[i].songID,
|
||||
calculatedData: {
|
||||
ktRating: i,
|
||||
},
|
||||
} as PBScoreDocument);
|
||||
}
|
||||
|
||||
await db["personal-bests"].insert(mockPBs);
|
||||
|
||||
const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/pbs?search=5.1.1.");
|
||||
|
||||
t.hasStrict(res.body, {
|
||||
success: true,
|
||||
description: "Retrieved 2 personal bests.",
|
||||
body: {
|
||||
pbs: [
|
||||
{
|
||||
chartID: "c2311194e3897ddb5745b1760d2c0141f933e683",
|
||||
},
|
||||
{
|
||||
chartID: "c641238220d73faf82659513ba03bde71b0b45f0",
|
||||
},
|
||||
],
|
||||
songs: [
|
||||
{
|
||||
title: "5.1.1.",
|
||||
},
|
||||
],
|
||||
charts: [
|
||||
{
|
||||
chartID: "c2311194e3897ddb5745b1760d2c0141f933e683",
|
||||
},
|
||||
{
|
||||
chartID: "c641238220d73faf82659513ba03bde71b0b45f0",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
@@ -4,13 +4,11 @@ import { CloseAllConnections } from "../../../../../../../../../test-utils/close
|
||||
import mockApi from "../../../../../../../../../test-utils/mock-api";
|
||||
import ResetDBState, { SetIndexesForDB } from "../../../../../../../../../test-utils/resets";
|
||||
import {
|
||||
SessionDocument,
|
||||
ScoreDocument,
|
||||
UserGoalDocument,
|
||||
GoalDocument,
|
||||
MilestoneDocument,
|
||||
UserMilestoneDocument,
|
||||
PBScoreDocument,
|
||||
} from "tachi-common";
|
||||
import {
|
||||
GetKTDataJSON,
|
||||
@@ -299,138 +297,6 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/scores/recent", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("GET /api/v1/users/:userID/games/:game/:playtype/pbs/best", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should return a users best 100 personal bests.", async (t) => {
|
||||
const mockPBs: PBScoreDocument[] = [];
|
||||
|
||||
for (let i = 0; i < 200; i++) {
|
||||
mockPBs.push({
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
isPrimary: true,
|
||||
chartID: i.toString(), // hack to generate some random chartIDs
|
||||
songID: Testing511Song.id,
|
||||
calculatedData: {
|
||||
ktRating: i,
|
||||
},
|
||||
} as PBScoreDocument);
|
||||
}
|
||||
|
||||
await db["personal-bests"].insert(mockPBs);
|
||||
|
||||
for (const sc of mockPBs) {
|
||||
delete sc._id; // lol
|
||||
}
|
||||
|
||||
const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/pbs/best");
|
||||
|
||||
t.hasStrict(res.body, {
|
||||
success: true,
|
||||
description: "Retrieved 100 personal bests.",
|
||||
body: {
|
||||
scores: mockPBs.slice(100).reverse(),
|
||||
songs: [Testing511Song],
|
||||
charts: [],
|
||||
},
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("GET /api/v1/users/:userID/games/:game/:playtype/pbs", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(LoadKTBlackIIDXData);
|
||||
|
||||
t.test("Should return 400 if no search param is given", async (t) => {
|
||||
const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/pbs");
|
||||
|
||||
t.equal(res.statusCode, 400);
|
||||
t.equal(res.body.success, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return 400 if invalid search param is given", async (t) => {
|
||||
const res = await mockApi.get(
|
||||
"/api/v1/users/test_zkldi/games/iidx/SP/pbs?search=foo&search=bar"
|
||||
);
|
||||
|
||||
t.equal(res.statusCode, 400);
|
||||
t.equal(res.body.success, false);
|
||||
|
||||
const res2 = await mockApi.get(
|
||||
"/api/v1/users/test_zkldi/games/iidx/SP/pbs?search[$where]=process.exit(1)"
|
||||
);
|
||||
|
||||
t.equal(res2.statusCode, 400);
|
||||
t.equal(res2.body.success, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should search a user's personal bests.", async (t) => {
|
||||
const mockPBs: PBScoreDocument[] = [];
|
||||
|
||||
const charts = GetKTDataJSON("./tachi/ktblack-charts-iidx.json");
|
||||
|
||||
for (let i = 0; i < 200; i++) {
|
||||
mockPBs.push({
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
isPrimary: true,
|
||||
chartID: charts[i].chartID,
|
||||
songID: charts[i].songID,
|
||||
calculatedData: {
|
||||
ktRating: i,
|
||||
},
|
||||
} as PBScoreDocument);
|
||||
}
|
||||
|
||||
await db["personal-bests"].insert(mockPBs);
|
||||
|
||||
const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/pbs?search=5.1.1.");
|
||||
|
||||
t.hasStrict(res.body, {
|
||||
success: true,
|
||||
description: "Retrieved 2 personal bests.",
|
||||
body: {
|
||||
pbs: [
|
||||
{
|
||||
chartID: "c2311194e3897ddb5745b1760d2c0141f933e683",
|
||||
},
|
||||
{
|
||||
chartID: "c641238220d73faf82659513ba03bde71b0b45f0",
|
||||
},
|
||||
],
|
||||
songs: [
|
||||
{
|
||||
title: "5.1.1.",
|
||||
},
|
||||
],
|
||||
charts: [
|
||||
{
|
||||
chartID: "c2311194e3897ddb5745b1760d2c0141f933e683",
|
||||
},
|
||||
{
|
||||
chartID: "c641238220d73faf82659513ba03bde71b0b45f0",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("GET /api/v1/users/:userID/games/:game/:playtype/scores", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(LoadKTBlackIIDXData);
|
||||
@@ -530,111 +396,6 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/scores", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(LoadKTBlackIIDXData);
|
||||
|
||||
t.test("Should return 400 if no search param is given", async (t) => {
|
||||
const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/sessions");
|
||||
|
||||
t.equal(res.statusCode, 400);
|
||||
t.equal(res.body.success, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return 400 if invalid search param is given", async (t) => {
|
||||
const res = await mockApi.get(
|
||||
"/api/v1/users/test_zkldi/games/iidx/SP/sessions?search=foo&search=bar"
|
||||
);
|
||||
|
||||
t.equal(res.statusCode, 400);
|
||||
t.equal(res.body.success, false);
|
||||
|
||||
// evil eval attempts
|
||||
const res2 = await mockApi.get(
|
||||
"/api/v1/users/test_zkldi/games/iidx/SP/sessions?search[$where]=process.exit(1)"
|
||||
);
|
||||
|
||||
t.equal(res2.statusCode, 400);
|
||||
t.equal(res2.body.success, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should search a user's sessions.", async (t) => {
|
||||
await db.sessions.insert(
|
||||
["Epic Session", "Session Of Epic", "Epic Gaming", "something else", "bad session"].map(
|
||||
(e) => ({
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
name: e,
|
||||
desc: "something",
|
||||
sessionID: e, // hack to avoid db nonsense
|
||||
})
|
||||
) as SessionDocument[]
|
||||
);
|
||||
|
||||
const res = await mockApi.get(
|
||||
"/api/v1/users/test_zkldi/games/iidx/SP/sessions?search=Epic"
|
||||
);
|
||||
|
||||
t.hasStrict(res.body, {
|
||||
success: true,
|
||||
description: "Retrieved 3 sessions.",
|
||||
body: [],
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions/best", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(LoadKTBlackIIDXData);
|
||||
|
||||
t.test("Should return a user's best 100 sessions.", async (t) => {
|
||||
const sessions: SessionDocument[] = [];
|
||||
|
||||
for (let i = 0; i < 200; i++) {
|
||||
sessions.push({
|
||||
sessionID: i.toString(),
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
calculatedData: {
|
||||
ktRating: i,
|
||||
},
|
||||
} as SessionDocument);
|
||||
}
|
||||
|
||||
await db.sessions.remove({});
|
||||
await db.sessions.insert(sessions);
|
||||
|
||||
const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/sessions/best");
|
||||
|
||||
t.hasStrict(res.body, {
|
||||
success: true,
|
||||
description: "Retrieved 100 sessions.",
|
||||
});
|
||||
|
||||
t.strictSame(
|
||||
res.body.body.map((e: SessionDocument) => e.sessionID),
|
||||
sessions
|
||||
.slice(100)
|
||||
.reverse()
|
||||
.map((e) => e.sessionID)
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("GET /api/v1/users/:userID/games/:game/:playtype/most-played", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
|
||||
@@ -12,14 +12,12 @@ import {
|
||||
integer,
|
||||
PBScoreDocument,
|
||||
} from "tachi-common";
|
||||
import {
|
||||
SearchGameSongsAndCharts,
|
||||
SearchSessions,
|
||||
} from "../../../../../../../../../lib/search/search";
|
||||
import { SearchGameSongsAndCharts } from "../../../../../../../../../lib/search/search";
|
||||
import { FilterChartsAndSongs } from "../../../../../../../../../utils/scores";
|
||||
import { CheckStrProfileAlg } from "../../../../../../../../../utils/string-checks";
|
||||
import { IsString } from "../../../../../../../../../utils/misc";
|
||||
import pbsRouter from "./pbs/router";
|
||||
import sessionsRouter from "./sessions/router";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
@@ -243,67 +241,6 @@ router.get("/scores/recent", async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Search a users sessions.
|
||||
*
|
||||
* @name GET /api/v1/users/:userID/games/:game/:playtype/sessions
|
||||
*/
|
||||
router.get("/sessions", async (req, res) => {
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser!;
|
||||
const game = req[SYMBOL_TachiData]!.game!;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype!;
|
||||
|
||||
if (typeof req.query.search !== "string") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid value of ${req.query.search} for search parameter.`,
|
||||
});
|
||||
}
|
||||
|
||||
const sessions = await SearchSessions(req.query.search, game, playtype, user.id, 100);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Retrieved ${sessions.length} sessions.`,
|
||||
body: sessions,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a user's best 100 sessions according to the default statistic
|
||||
* for that game.
|
||||
*
|
||||
* @param alg - An override to specify a different algorithm for that game.
|
||||
* UNIMPLEMENTED!!!
|
||||
* @name GET /api/v1/users/:userID/games/:game/:playtype/sessions/best
|
||||
*/
|
||||
router.get("/sessions/best", async (req, res) => {
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser!;
|
||||
const game = req[SYMBOL_TachiData]!.game!;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype!;
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const sessions = await db.sessions.find(
|
||||
{
|
||||
userID: user.id,
|
||||
game,
|
||||
playtype,
|
||||
},
|
||||
{
|
||||
limit: 100,
|
||||
sort: {
|
||||
[`calculatedData.${gptConfig.defaultSessionRatingAlg}`]: -1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Retrieved ${sessions.length} sessions.`,
|
||||
body: sessions,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the users most played charts by playcount.
|
||||
*
|
||||
@@ -458,5 +395,6 @@ router.get("/leaderboard-adjacent", async (req, res) => {
|
||||
});
|
||||
|
||||
router.use("/pbs", pbsRouter);
|
||||
router.use("/sessions", sessionsRouter);
|
||||
|
||||
export default router;
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import t from "tap";
|
||||
import db from "../../../../../../../../../../external/mongo/db";
|
||||
import { CloseAllConnections } from "../../../../../../../../../../test-utils/close-connections";
|
||||
import mockApi from "../../../../../../../../../../test-utils/mock-api";
|
||||
import ResetDBState from "../../../../../../../../../../test-utils/resets";
|
||||
import { LoadKTBlackIIDXData } from "../../../../../../../../../../test-utils/test-data";
|
||||
import { SessionDocument } from "tachi-common";
|
||||
|
||||
t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(LoadKTBlackIIDXData);
|
||||
|
||||
t.test("Should return 400 if no search param is given", async (t) => {
|
||||
const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/sessions");
|
||||
|
||||
t.equal(res.statusCode, 400);
|
||||
t.equal(res.body.success, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return 400 if invalid search param is given", async (t) => {
|
||||
const res = await mockApi.get(
|
||||
"/api/v1/users/test_zkldi/games/iidx/SP/sessions?search=foo&search=bar"
|
||||
);
|
||||
|
||||
t.equal(res.statusCode, 400);
|
||||
t.equal(res.body.success, false);
|
||||
|
||||
// evil eval attempts
|
||||
const res2 = await mockApi.get(
|
||||
"/api/v1/users/test_zkldi/games/iidx/SP/sessions?search[$where]=process.exit(1)"
|
||||
);
|
||||
|
||||
t.equal(res2.statusCode, 400);
|
||||
t.equal(res2.body.success, false);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should search a user's sessions.", async (t) => {
|
||||
await db.sessions.insert(
|
||||
["Epic Session", "Session Of Epic", "Epic Gaming", "something else", "bad session"].map(
|
||||
(e) => ({
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
name: e,
|
||||
desc: "something",
|
||||
sessionID: e, // hack to avoid db nonsense
|
||||
})
|
||||
) as SessionDocument[]
|
||||
);
|
||||
|
||||
const res = await mockApi.get(
|
||||
"/api/v1/users/test_zkldi/games/iidx/SP/sessions?search=Epic"
|
||||
);
|
||||
|
||||
t.hasStrict(res.body, {
|
||||
success: true,
|
||||
description: "Retrieved 3 sessions.",
|
||||
body: [],
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("GET /api/v1/users/:userID/games/:game/:playtype/sessions/best", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(LoadKTBlackIIDXData);
|
||||
|
||||
t.test("Should return a user's best 100 sessions.", async (t) => {
|
||||
const sessions: SessionDocument[] = [];
|
||||
|
||||
for (let i = 0; i < 200; i++) {
|
||||
sessions.push({
|
||||
sessionID: i.toString(),
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
userID: 1,
|
||||
calculatedData: {
|
||||
ktRating: i,
|
||||
},
|
||||
} as SessionDocument);
|
||||
}
|
||||
|
||||
await db.sessions.remove({});
|
||||
await db.sessions.insert(sessions);
|
||||
|
||||
const res = await mockApi.get("/api/v1/users/test_zkldi/games/iidx/SP/sessions/best");
|
||||
|
||||
t.hasStrict(res.body, {
|
||||
success: true,
|
||||
description: "Retrieved 100 sessions.",
|
||||
});
|
||||
|
||||
t.strictSame(
|
||||
res.body.body.map((e: SessionDocument) => e.sessionID),
|
||||
sessions
|
||||
.slice(100)
|
||||
.reverse()
|
||||
.map((e) => e.sessionID)
|
||||
);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.teardown(CloseAllConnections);
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { Router } from "express";
|
||||
import db from "../../../../../../../../../../external/mongo/db";
|
||||
import { SYMBOL_TachiData } from "../../../../../../../../../../lib/constants/tachi";
|
||||
import { SearchSessions } from "../../../../../../../../../../lib/search/search";
|
||||
import { GetGamePTConfig } from "tachi-common";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
/**
|
||||
* Search a users sessions.
|
||||
*
|
||||
* @name GET /api/v1/users/:userID/games/:game/:playtype/sessions
|
||||
*/
|
||||
router.get("/", async (req, res) => {
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser!;
|
||||
const game = req[SYMBOL_TachiData]!.game!;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype!;
|
||||
|
||||
if (typeof req.query.search !== "string") {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid value of ${req.query.search} for search parameter.`,
|
||||
});
|
||||
}
|
||||
|
||||
const sessions = await SearchSessions(req.query.search, game, playtype, user.id, 100);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Retrieved ${sessions.length} sessions.`,
|
||||
body: sessions,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a user's best 100 sessions according to the default statistic
|
||||
* for that game.
|
||||
*
|
||||
* @param alg - An override to specify a different algorithm for that game.
|
||||
* UNIMPLEMENTED!!!
|
||||
* @name GET /api/v1/users/:userID/games/:game/:playtype/sessions/best
|
||||
*/
|
||||
router.get("/best", async (req, res) => {
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser!;
|
||||
const game = req[SYMBOL_TachiData]!.game!;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype!;
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const sessions = await db.sessions.find(
|
||||
{
|
||||
userID: user.id,
|
||||
game,
|
||||
playtype,
|
||||
},
|
||||
{
|
||||
limit: 100,
|
||||
sort: {
|
||||
[`calculatedData.${gptConfig.defaultSessionRatingAlg}`]: -1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Retrieved ${sessions.length} sessions.`,
|
||||
body: sessions,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a users 100 most recent highlighted sessions. Returned in timeEnded order.
|
||||
*
|
||||
* @name GET /api/v1/users/:userID/games/:game/:playtype/sessions/highlighted
|
||||
*/
|
||||
router.get("/highlighted", async (req, res) => {
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser!;
|
||||
const game = req[SYMBOL_TachiData]!.game!;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype!;
|
||||
|
||||
const sessions = await db.sessions.find(
|
||||
{ userID: user.id, game, playtype, highlight: true },
|
||||
{ sort: { timeEnded: -1 }, limit: 100 }
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Returned ${sessions.length} sessions.`,
|
||||
body: sessions,
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
integer,
|
||||
SessionDocument,
|
||||
ScoreDocument,
|
||||
ChartDocument,
|
||||
PublicUserDocument,
|
||||
@@ -55,4 +56,5 @@ export interface TachiRequestData {
|
||||
chartDoc?: AnyChartDocument;
|
||||
songDoc?: AnySongDocument;
|
||||
scoreDoc?: ScoreDocument;
|
||||
sessionDoc?: SessionDocument;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user