Define new score and session routes.

This commit is contained in:
zkldi
2021-06-27 15:14:14 +01:00
parent 2cc916ff40
commit 116e75c0ec
16 changed files with 640 additions and 316 deletions
+4 -4
View File
@@ -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:
+1 -1
View File
@@ -62,7 +62,7 @@ monkDB
});
const RemoveIDMiddleware: TMiddleware =
({ collection, monkInstance }) =>
() =>
(next) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(args: any, method) => {
+7 -2
View File
@@ -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;
@@ -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;
@@ -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);
@@ -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;
+2
View File
@@ -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;
}