diff --git a/server/package.json b/server/package.json index 191f87408..e89012acd 100644 --- a/server/package.json +++ b/server/package.json @@ -84,7 +84,7 @@ "rimraf": "3.0.2", "safe-json-stringify": "1.2.0", "seq-logging": "1.1.1", - "tachi-common": "0.7.7", + "tachi-common": "0.7.8", "ts-node": "10.4.0", "tsconfig-paths": "3.12.0", "typescript": "4.5.5", diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml index ff0e79f75..1534ed770 100644 --- a/server/pnpm-lock.yaml +++ b/server/pnpm-lock.yaml @@ -65,7 +65,7 @@ specifiers: safe-json-stringify: 1.2.0 seq-logging: 1.1.1 supertest: 6.2.2 - tachi-common: 0.7.7 + tachi-common: 0.7.8 tap: 15.1.6 ts-node: 10.4.0 tsconfig-paths: 3.12.0 @@ -112,7 +112,7 @@ dependencies: rimraf: 3.0.2 safe-json-stringify: 1.2.0 seq-logging: 1.1.1 - tachi-common: 0.7.7 + tachi-common: 0.7.8 ts-node: 10.4.0_2615db9039ce432b4abf2fc39ef336ec tsconfig-paths: 3.12.0 typescript: 4.5.5 @@ -5363,8 +5363,8 @@ packages: engines: {node: '>= 0.4'} dev: true - /tachi-common/0.7.7: - resolution: {integrity: sha512-WsV4bCKdhackC24aTt7vdJTz/IEdRHiSm3+NeDWlU3AOmdMQV8YTcMCjeNscTr4yoccjpRNNG/avs+h/fFLDeg==} + /tachi-common/0.7.8: + resolution: {integrity: sha512-rDT+scIl1WQacdPrEIvM7fenRvkdjEQwMJMBDz1Pf/VQoYvyBeRL0rxjHrd26L8qkg1yPhwVKhs9DZUMnt4QzA==} dependencies: '@types/mongodb': 3.6.18 dev: false diff --git a/server/src/lib/targets/milestones.ts b/server/src/lib/targets/milestones.ts index b99fae9e9..f538f77b2 100644 --- a/server/src/lib/targets/milestones.ts +++ b/server/src/lib/targets/milestones.ts @@ -59,7 +59,7 @@ export function CalculateMilestoneOutOf(milestone: MilestoneDocument) { if (milestone.criteria.type === "all") { return goalIDs.length; - } else if (milestone.criteria.type === "abs") { + } else if (milestone.criteria.type === "total") { if (milestone.criteria.value === null) { throw new Error( `Invalid milestone ${milestone.milestoneID} - abs and null are not compatible.` @@ -67,17 +67,10 @@ export function CalculateMilestoneOutOf(milestone: MilestoneDocument) { } return milestone.criteria.value!; - } else if (milestone.criteria.type === "proportion") { - if (milestone.criteria.value === null) { - throw new Error( - `Invalid milestone ${milestone.milestoneID} - proportion and null are not compatible.` - ); - } - - return Math.floor(milestone.criteria.value * goalIDs.length); } throw new Error( + // @ts-expect-error Yeah obviously this shouldn't happen. Mayaswell throw though. `Invalid milestone.criteria.type of ${milestone.criteria.type} -- milestoneID ${milestone.milestoneID}` ); } diff --git a/server/src/server/router/api/v1/games/_game/_playtype/targets/goals/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/targets/goals/router.ts index 06e396d55..f30571f43 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/targets/goals/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/targets/goals/router.ts @@ -108,7 +108,7 @@ router.get("/:goalID", ResolveGoalID, async (req, res) => { * * @param userID - The userID to evaluate this goal against. Must be a player of this GPT. * - * @name GET /api/v1/games/:game/:playtype/targets/goals/:goalID/evaluate + * @name GET /api/v1/games/:game/:playtype/targets/goals/:goalID/evaluate-for */ router.get( "/:goalID/evaluate-for", diff --git a/server/src/server/router/api/v1/games/_game/_playtype/targets/milestones/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/targets/milestones/router.ts new file mode 100644 index 000000000..c037cde72 --- /dev/null +++ b/server/src/server/router/api/v1/games/_game/_playtype/targets/milestones/router.ts @@ -0,0 +1,117 @@ +import { RequestHandler, Router } from "express"; +import db from "external/mongo/db"; +import { SYMBOL_TachiAPIAuth, SYMBOL_TachiData } from "lib/constants/tachi"; +import { EvaluateMilestoneProgress, GetGoalsInMilestone } from "lib/targets/milestones"; +import prValidate from "server/middleware/prudence-validate"; +import { FormatGame } from "tachi-common"; +import { AssignToReqTachiData } from "utils/req-tachi-data"; +import { GetUsersWithIDs, ResolveUser } from "utils/user"; + +const router: Router = Router({ mergeParams: true }); + +const ResolveMilestoneID: RequestHandler = async (req, res, next) => { + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; + const milestoneID = req.params.milestoneID; + + const milestone = await db.milestones.findOne({ + milestoneID, + game, + playtype, + }); + + if (!milestone) { + return res.status(404).json({ + success: false, + description: `A milestone with ID ${milestoneID} doesn't exist.`, + }); + } + + AssignToReqTachiData(req, { milestoneDoc: milestone }); + + return next(); +}; + +/** + * Retrieve information about this milestone and who is subscribed to it. + * + * @name GET /api/v1/games/:game/:playtype/targets/milestones/:milestoneID + */ +router.get("/:milestoneID", ResolveMilestoneID, async (req, res) => { + const milestone = req[SYMBOL_TachiData]!.milestoneDoc!; + + const milestoneSubs = await db["milestone-subs"].find({ + milestoneID: milestone.milestoneID, + }); + + const users = await GetUsersWithIDs(milestoneSubs.map((e) => e.userID)); + + const goals = await GetGoalsInMilestone(milestone); + + return res.status(200).json({ + success: true, + description: `Retrieved information about ${milestone.name}.`, + body: { + milestone, + milestoneSubs, + users, + goals, + }, + }); +}); + +/** + * Evaluates a milestone upon a user, even if they aren't subscribed to it. + * + * @param userID - The userID to evaluate this goal against. Must be a player of this GPT. + * + * @name GET /api/v1/games/:game/:playtype/targets/milestones/:milestone-id/evaluate-for + */ +router.get( + "/:milestoneID/evaluate-for", + ResolveMilestoneID, + prValidate({ userID: "string" }), + async (req, res) => { + const game = req[SYMBOL_TachiData]!.game!; + const playtype = req[SYMBOL_TachiData]!.playtype!; + + const userID = req.query.userID as string; + + const user = await ResolveUser(userID); + + if (!user) { + return res.status(404).json({ + success: false, + description: `The user ${userID} does not exist.`, + }); + } + + const hasPlayed = await db["game-stats"].findOne({ + game, + playtype, + userID: user.id, + }); + + if (!hasPlayed) { + return res.status(400).json({ + success: false, + description: `The user ${user.username} hasn't played ${FormatGame( + game, + playtype + )}.`, + }); + } + + const milestone = req[SYMBOL_TachiData]!.milestoneDoc!; + + const milestoneProgress = await EvaluateMilestoneProgress(user.id, milestone); + + return res.status(200).json({ + success: true, + description: `Evaluated ${milestone.name} for ${user.username}.`, + body: milestoneProgress, + }); + } +); + +export default router; diff --git a/server/src/server/router/api/v1/games/_game/_playtype/targets/router.ts b/server/src/server/router/api/v1/games/_game/_playtype/targets/router.ts index 63929423c..9ba386131 100644 --- a/server/src/server/router/api/v1/games/_game/_playtype/targets/router.ts +++ b/server/src/server/router/api/v1/games/_game/_playtype/targets/router.ts @@ -1,8 +1,10 @@ import { Router } from "express"; import goalsRouter from "./goals/router"; +import milestonesRouter from "./milestones/router"; const router: Router = Router({ mergeParams: true }); router.use("/goals", goalsRouter); +router.use("/milestones", milestonesRouter); export default router; diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts index f5339826a..dc69e0eed 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/targets/goals/router.ts @@ -18,7 +18,7 @@ const router: Router = Router({ mergeParams: true }); const logger = CreateLogCtx(__filename); /** - * Retrieves this users' set goals. + * Retrieves this users' set goals for this GPT. * * @name GET /api/v1/users/:userID/games/:game/:playtype/targets/goals */