mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-29 18:37:58 +03:00
Merge pull request #355 from TeamNewGuys:zkldi/issue-354-Add-class-achievement-APIs
Add class achievement APIs.
This commit is contained in:
+1
-1
@@ -74,7 +74,7 @@
|
||||
"redis": "3.1.2",
|
||||
"rimraf": "3.0.2",
|
||||
"safe-json-stringify": "1.2.0",
|
||||
"tachi-common": "0.2.19",
|
||||
"tachi-common": "0.2.25",
|
||||
"typescript": "4.3.4",
|
||||
"winston": "3.3.3",
|
||||
"winston-transport": "^4.4.0"
|
||||
|
||||
Generated
+4
-4
@@ -55,7 +55,7 @@ specifiers:
|
||||
rimraf: 3.0.2
|
||||
safe-json-stringify: 1.2.0
|
||||
supertest: 6.1.3
|
||||
tachi-common: 0.2.19
|
||||
tachi-common: 0.2.25
|
||||
tap: 15.0.9
|
||||
ts-node: 10.0.0
|
||||
tsconfig-paths: 3.10.1
|
||||
@@ -91,7 +91,7 @@ dependencies:
|
||||
redis: 3.1.2
|
||||
rimraf: 3.0.2
|
||||
safe-json-stringify: 1.2.0
|
||||
tachi-common: 0.2.19
|
||||
tachi-common: 0.2.25
|
||||
typescript: 4.3.4
|
||||
winston: 3.3.3
|
||||
winston-transport: 4.4.0
|
||||
@@ -3910,8 +3910,8 @@ packages:
|
||||
strip-ansi: 6.0.0
|
||||
dev: true
|
||||
|
||||
/tachi-common/0.2.19:
|
||||
resolution: {integrity: sha512-Gy4tr1XmwgpIfJqAKk6PUxTETg+4N00mLrSNBqBwKvRfINBBI6vQ5wp98PHTaCEE8h+dKkv1Uj0u0VnCRrLZig==}
|
||||
/tachi-common/0.2.25:
|
||||
resolution: {integrity: sha512-FSMwEWWcvP5Vw2heSD6vD7Iz1/lI+bO1HF0+0iURw5Ybj16yJZQ9wBiaIz2hotNweMhlQ73v57tj2TMUmFyG9w==}
|
||||
dependencies:
|
||||
monk: 7.3.4
|
||||
transitivePeerDependencies:
|
||||
|
||||
Vendored
+4
-1
@@ -33,6 +33,7 @@ import {
|
||||
integer,
|
||||
FervidexSettingsDocument,
|
||||
OrphanChart,
|
||||
ClassAchievementDocument,
|
||||
} from "tachi-common";
|
||||
import monk, { TMiddleware } from "monk";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
@@ -177,6 +178,7 @@ const db = {
|
||||
"orphan-chart-queue": monkDB.get<OrphanChart>("orphan-chart-queue"),
|
||||
"password-reset-codes":
|
||||
monkDB.get<{ code: string; userID: integer; createdOn: number }>("password-reset-codes"),
|
||||
"class-achievements": monkDB.get<ClassAchievementDocument>("class-achievements"),
|
||||
};
|
||||
|
||||
export type StaticDatabases =
|
||||
@@ -209,7 +211,8 @@ export type StaticDatabases =
|
||||
| "orphan-chart-queue"
|
||||
| "password-reset-codes"
|
||||
| "user-settings"
|
||||
| "counters";
|
||||
| "counters"
|
||||
| "class-achievements";
|
||||
|
||||
export type Databases = StaticDatabases | `songs-${Game}` | `charts-${Game}`;
|
||||
|
||||
|
||||
Vendored
+1
@@ -81,6 +81,7 @@ const staticIndexes: Partial<Record<Databases, Index[]>> = {
|
||||
"user-private-information": [index({ userID: 1 }, UNIQUE), index({ email: 1 }, UNIQUE)],
|
||||
"fer-settings": [index({ userID: 1 }, UNIQUE)],
|
||||
counters: [index({ counterName: 1 }, UNIQUE)],
|
||||
"class-achievements": [index({ game: 1, playtype: 1, timeAchieved: 1 })],
|
||||
};
|
||||
|
||||
const indexes: Partial<Record<Databases, Index[]>> = staticIndexes;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { UserGameStats } from "tachi-common";
|
||||
import t from "tap";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { CalculateClassDeltas, UpdateUGSClasses } from "./classes";
|
||||
import { ProcessClassDeltas, UpdateUGSClasses } from "./classes";
|
||||
|
||||
import { GitadoraColours } from "lib/constants/classes";
|
||||
|
||||
@@ -45,14 +45,15 @@ t.test("#UpdateUGSClasses", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("#CalculateClassDeltas", (t) => {
|
||||
t.test("#ProcessClassDeltas", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should return improved classes from null", (t) => {
|
||||
const res = CalculateClassDeltas("SP", { dan: 18 }, null, 1, logger);
|
||||
t.test("Should return improved classes from null", async (t) => {
|
||||
const res = await ProcessClassDeltas("iidx", "SP", { dan: 18 }, null, 1, logger);
|
||||
|
||||
t.strictSame(res, [
|
||||
{
|
||||
game: "iidx",
|
||||
set: "dan",
|
||||
playtype: "SP",
|
||||
old: null,
|
||||
@@ -63,8 +64,9 @@ t.test("#CalculateClassDeltas", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return improved classes from null class", (t) => {
|
||||
const res = CalculateClassDeltas(
|
||||
t.test("Should return improved classes from null class", async (t) => {
|
||||
const res = await ProcessClassDeltas(
|
||||
"iidx",
|
||||
"SP",
|
||||
{ dan: 18 },
|
||||
{ classes: {} } as UserGameStats,
|
||||
@@ -74,6 +76,7 @@ t.test("#CalculateClassDeltas", (t) => {
|
||||
|
||||
t.strictSame(res, [
|
||||
{
|
||||
game: "iidx",
|
||||
set: "dan",
|
||||
playtype: "SP",
|
||||
old: null,
|
||||
@@ -84,8 +87,9 @@ t.test("#CalculateClassDeltas", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should return improved classes", (t) => {
|
||||
const res = CalculateClassDeltas(
|
||||
t.test("Should return improved classes", async (t) => {
|
||||
const res = await ProcessClassDeltas(
|
||||
"iidx",
|
||||
"SP",
|
||||
{ dan: 18 },
|
||||
{ classes: { dan: 17 } } as unknown as UserGameStats,
|
||||
@@ -95,6 +99,7 @@ t.test("#CalculateClassDeltas", (t) => {
|
||||
|
||||
t.strictSame(res, [
|
||||
{
|
||||
game: "iidx",
|
||||
set: "dan",
|
||||
playtype: "SP",
|
||||
old: 17,
|
||||
@@ -105,8 +110,9 @@ t.test("#CalculateClassDeltas", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should not return identical classes", (t) => {
|
||||
const res = CalculateClassDeltas(
|
||||
t.test("Should not return identical classes", async (t) => {
|
||||
const res = await ProcessClassDeltas(
|
||||
"iidx",
|
||||
"SP",
|
||||
{ dan: 18 },
|
||||
{ classes: { dan: 18 } } as unknown as UserGameStats,
|
||||
@@ -119,8 +125,9 @@ t.test("#CalculateClassDeltas", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
t.test("Should not return worse classes", (t) => {
|
||||
const res = CalculateClassDeltas(
|
||||
t.test("Should not return worse classes", async (t) => {
|
||||
const res = await ProcessClassDeltas(
|
||||
"iidx",
|
||||
"SP",
|
||||
{ dan: 16 },
|
||||
{ classes: { dan: 18 } } as unknown as UserGameStats,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Game, Playtypes, integer, UserGameStats, ClassDelta, IDStrings } from "tachi-common";
|
||||
import { GameClasses } from "tachi-common/js/game-classes";
|
||||
import deepmerge from "deepmerge";
|
||||
import db from "external/mongo/db";
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import { EmitWebhookEvent } from "lib/webhooks/webhooks";
|
||||
import { ClassDelta, Game, IDStrings, integer, Playtypes, UserGameStats } from "tachi-common";
|
||||
import { GameClasses } from "tachi-common/js/game-classes";
|
||||
import { ReturnClassIfGreater } from "utils/class";
|
||||
import {
|
||||
CalculateChunithmColour,
|
||||
CalculateGitadoraColour,
|
||||
CalculateSDVXClass,
|
||||
} from "./builtin-class-handlers";
|
||||
import { ReturnClassIfGreater } from "utils/class";
|
||||
import { RedisPub } from "external/redis/redis-IPC";
|
||||
import { ClassHandler, ScoreClasses } from "./types";
|
||||
import { EmitWebhookEvent } from "lib/webhooks/webhooks";
|
||||
|
||||
type ClassHandlerMap = {
|
||||
[G in Game]:
|
||||
@@ -110,17 +110,20 @@ export async function UpdateUGSClasses(
|
||||
* This is for calculating scenarios where a users class has improved (i.e. they have gone from 9th dan to 10th dan).
|
||||
*
|
||||
* Knowing this information allows us to attach it onto the import, and also emit things on redis
|
||||
* so that other services can listen for it. In the future we might allow webhooks, too.
|
||||
* s**This function emits webhook events and inserts classachieved documents into the DB!**
|
||||
*/
|
||||
export function CalculateClassDeltas(
|
||||
export async function ProcessClassDeltas(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game],
|
||||
classes: ScoreClasses,
|
||||
userGameStats: UserGameStats | null,
|
||||
userID: integer,
|
||||
logger: KtLogger
|
||||
): ClassDelta[] {
|
||||
): Promise<ClassDelta[]> {
|
||||
const deltas: ClassDelta[] = [];
|
||||
|
||||
const updateOps = [];
|
||||
|
||||
for (const s in classes) {
|
||||
const classSet = s as keyof GameClasses<IDStrings>;
|
||||
const classVal = classes[classSet];
|
||||
@@ -139,6 +142,7 @@ export function CalculateClassDeltas(
|
||||
let delta: ClassDelta;
|
||||
if (isGreater === null) {
|
||||
delta = {
|
||||
game,
|
||||
set: classSet,
|
||||
playtype,
|
||||
old: null,
|
||||
@@ -146,6 +150,7 @@ export function CalculateClassDeltas(
|
||||
};
|
||||
} else {
|
||||
delta = {
|
||||
game,
|
||||
set: classSet,
|
||||
playtype,
|
||||
old: userGameStats!.classes[classSet]!,
|
||||
@@ -155,6 +160,16 @@ export function CalculateClassDeltas(
|
||||
|
||||
EmitWebhookEvent({ type: "class-update/v1", content: { userID, ...delta } });
|
||||
|
||||
updateOps.push({
|
||||
userID,
|
||||
classSet: delta.set,
|
||||
classOldValue: delta.old,
|
||||
classValue: delta.new,
|
||||
game,
|
||||
playtype,
|
||||
timeAchieved: Date.now(),
|
||||
});
|
||||
|
||||
deltas.push(delta);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -162,5 +177,7 @@ export function CalculateClassDeltas(
|
||||
}
|
||||
}
|
||||
|
||||
await db["class-achievements"].insert(updateOps);
|
||||
|
||||
return deltas;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Game, Playtypes, integer, UserGameStats, ClassDelta } from "tachi-commo
|
||||
import db from "external/mongo/db";
|
||||
import { CreateGameSettings } from "lib/game-settings/create-game-settings";
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import { CalculateClassDeltas, UpdateUGSClasses } from "./classes";
|
||||
import { ProcessClassDeltas, UpdateUGSClasses } from "./classes";
|
||||
import { CalculateRatings } from "./rating";
|
||||
import { ClassHandler } from "./types";
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function UpdateUsersGamePlaytypeStats(
|
||||
|
||||
logger.debug(`Calculating Class Deltas...`);
|
||||
|
||||
const deltas = CalculateClassDeltas(playtype, classes, userGameStats, userID, logger);
|
||||
const deltas = await ProcessClassDeltas(game, playtype, classes, userGameStats, userID, logger);
|
||||
|
||||
logger.debug(`Had ${deltas.length} deltas.`);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import { FilterQuery } from "mongodb";
|
||||
import { SYMBOL_TachiData } from "lib/constants/tachi";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { SearchUsersRegExp } from "lib/search/search";
|
||||
@@ -7,7 +8,7 @@ import { IsString } from "utils/misc";
|
||||
import { ParseStrPositiveNonZeroInt } from "utils/string-checks";
|
||||
import { GetUsersWithIDs } from "utils/user";
|
||||
import { ValidateAndGetChart } from "./middleware";
|
||||
import { FormatChart } from "tachi-common";
|
||||
import { FormatChart, FolderDocument } from "tachi-common";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -49,6 +50,44 @@ router.get("/", async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns any folders that contain this chart.
|
||||
*
|
||||
* @param inactive - Also include inactive folders.
|
||||
*
|
||||
* @name GET /api/v1/games/:game/:playtype/charts/:chartID/folders
|
||||
*/
|
||||
router.get("/folders", async (req, res) => {
|
||||
const chart = req[SYMBOL_TachiData]!.chartDoc!;
|
||||
|
||||
const folderIDs = await db["folder-chart-lookup"].find(
|
||||
{
|
||||
chartID: chart.chartID,
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
folderID: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const query: FilterQuery<FolderDocument> = {
|
||||
folderID: { $in: folderIDs.map((e) => e.folderID) },
|
||||
};
|
||||
|
||||
if (!req.query.inactive) {
|
||||
query.inactive = false;
|
||||
}
|
||||
|
||||
const folders = await db.folders.find(query);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Found ${folders.length} folders that contain this chart.`,
|
||||
body: folders,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the total amount of unique players that have played this chart.
|
||||
*
|
||||
|
||||
@@ -11,7 +11,9 @@ t.test("GET /api/v1/games/:game/:playtype", (t) => {
|
||||
|
||||
t.strictSame(res.body.body.config, GetGamePTConfig("iidx", "SP"));
|
||||
|
||||
// something else needs to go here?
|
||||
t.equal(res.body.body.chartCount, 1);
|
||||
t.equal(res.body.body.playerCount, 1);
|
||||
t.equal(res.body.body.scoreCount, 1);
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router } from "express";
|
||||
import { SYMBOL_TachiData } from "lib/constants/tachi";
|
||||
import { IsString } from "utils/misc";
|
||||
import { GetGamePTConfig, UserGameStats, FormatGame } from "tachi-common";
|
||||
import { GetGamePTConfig, UserGameStats, FormatGame, Game, Playtypes, integer } from "tachi-common";
|
||||
import { FindOptions } from "monk";
|
||||
import db from "external/mongo/db";
|
||||
import { CheckStrProfileAlg, ParseStrPositiveNonZeroInt } from "utils/string-checks";
|
||||
@@ -11,24 +11,66 @@ import songIDRouter from "./songs/_songID/router";
|
||||
import { ValidatePlaytypeFromParam } from "./middleware";
|
||||
import foldersRouter from "./folders/router";
|
||||
import tablesRouter from "./tables/router";
|
||||
import NodeCache from "node-cache";
|
||||
import { ONE_HOUR } from "lib/constants/time";
|
||||
import prValidate from "server/middleware/prudence-validate";
|
||||
import p from "prudence";
|
||||
import { GetClassDistribution } from "utils/queries/stats";
|
||||
import scoresRouter from "./scores/router";
|
||||
import { GetRelevantSongsAndCharts } from "utils/db";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
router.use(ValidatePlaytypeFromParam);
|
||||
|
||||
const gptStatCache = new NodeCache();
|
||||
|
||||
async function GetGameStats(
|
||||
game: Game,
|
||||
playtype: Playtypes[Game]
|
||||
): Promise<{ scoreCount: integer; playerCount: integer; chartCount: integer }> {
|
||||
const cacheRes = gptStatCache.get(`${game}:${playtype}`);
|
||||
|
||||
if (!cacheRes) {
|
||||
const [scoreCount, playerCount, chartCount] = await Promise.all([
|
||||
db.scores.count({
|
||||
game,
|
||||
playtype,
|
||||
}),
|
||||
db["game-stats"].count({
|
||||
game,
|
||||
playtype,
|
||||
}),
|
||||
db.charts[game].count({ playtype }),
|
||||
]);
|
||||
|
||||
gptStatCache.set(`${game}:${playtype}`, { scoreCount, playerCount, chartCount }, ONE_HOUR);
|
||||
|
||||
return { scoreCount, playerCount, chartCount };
|
||||
}
|
||||
|
||||
return cacheRes as { scoreCount: integer; playerCount: integer; chartCount: integer };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration for this game.
|
||||
* Returns the configuration for this game along with some statistics.
|
||||
*
|
||||
* @name GET /api/v1/games/:game/:playtype
|
||||
*/
|
||||
router.get("/", (req, res) => {
|
||||
router.get("/", async (req, res) => {
|
||||
const game = req[SYMBOL_TachiData]!.game!;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype!;
|
||||
|
||||
const { scoreCount, playerCount, chartCount } = await GetGameStats(game, playtype);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Retrieved information about ${FormatGame(game, playtype)}`,
|
||||
body: {
|
||||
config: GetGamePTConfig(game, playtype),
|
||||
scoreCount,
|
||||
playerCount,
|
||||
chartCount,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -49,6 +91,13 @@ router.get("/leaderboard", async (req, res) => {
|
||||
|
||||
const limit = ParseStrPositiveNonZeroInt(req.query.limit) ?? 50;
|
||||
|
||||
if (limit > 50) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid limit. Limit is capped at 50.`,
|
||||
});
|
||||
}
|
||||
|
||||
let alg = gptConfig.defaultProfileRatingAlg;
|
||||
if (IsString(req.query.alg)) {
|
||||
const temp = CheckStrProfileAlg(game, playtype, req.query.alg);
|
||||
@@ -92,11 +141,152 @@ router.get("/leaderboard", async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the best scores for this game.
|
||||
*
|
||||
* @param alg - An alternative algorithm to use instead of the gpts default.
|
||||
* @param limit - How many scores to return.
|
||||
*
|
||||
* @name GET /api/v1/games/:game/:playtype/score-leaderboard
|
||||
*/
|
||||
router.get("/score-leaderboard", async (req, res) => {
|
||||
const game = req[SYMBOL_TachiData]!.game!;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype!;
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const limit = ParseStrPositiveNonZeroInt(req.query.limit) ?? 50;
|
||||
|
||||
let alg = gptConfig.defaultProfileRatingAlg;
|
||||
if (IsString(req.query.alg)) {
|
||||
const temp = CheckStrProfileAlg(game, playtype, req.query.alg);
|
||||
|
||||
if (temp === null) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid value of ${
|
||||
req.query.alg
|
||||
} for alg. Expected one of ${gptConfig.profileRatingAlgs.join(", ")}`,
|
||||
});
|
||||
}
|
||||
|
||||
alg = temp;
|
||||
}
|
||||
|
||||
const pbs = await db["personal-bests"].find(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
[`calculatedData.${alg}`]: -1,
|
||||
},
|
||||
limit,
|
||||
}
|
||||
);
|
||||
|
||||
const users = await GetUsersWithIDs(pbs.map((e) => e.userID));
|
||||
|
||||
const { songs, charts } = await GetRelevantSongsAndCharts(pbs, game);
|
||||
|
||||
return res.status(200).send({
|
||||
success: true,
|
||||
description: `Successfully returned ${pbs.length} pbs.`,
|
||||
body: {
|
||||
pbs,
|
||||
songs,
|
||||
charts,
|
||||
users,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Return the distribution of players for the provided class.
|
||||
*
|
||||
* @param class - This should be one of the games supported classes.
|
||||
*
|
||||
* @name GET /api/v1/games/:game/:playtype/player-distribution
|
||||
*/
|
||||
router.get(
|
||||
"/class-distribution",
|
||||
prValidate({
|
||||
class: "string",
|
||||
}),
|
||||
async (req, res) => {
|
||||
const game = req[SYMBOL_TachiData]!.game!;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype!;
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const stat = req.query.class as string;
|
||||
|
||||
const supportedClasses = Object.keys(gptConfig.classHumanisedFormat);
|
||||
if (!supportedClasses.includes(stat)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid stat ${stat}. Expected any of ${supportedClasses.join(
|
||||
", "
|
||||
)}.`,
|
||||
});
|
||||
}
|
||||
|
||||
// @hack This is asserted above. We're just going to cast as any because we know what
|
||||
// we're doing.
|
||||
const distribution = await GetClassDistribution(game, playtype, stat as any);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Successfully retrieved distribution.`,
|
||||
body: distribution,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns recent class improvements for this GPT.
|
||||
*
|
||||
* @param limit - How many to return. Defaults to 10, caps at 50.
|
||||
*
|
||||
* @name GET /api/v1/games/:game/:playtype/recent-classes
|
||||
*/
|
||||
router.get(
|
||||
"/recent-classes",
|
||||
prValidate({
|
||||
limit: p.optional((self) => p.isBoundedInteger(1, 50)(Number(self))),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const game = req[SYMBOL_TachiData]!.game!;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype!;
|
||||
|
||||
const limit = req.query.limit ? Number(req.query.limit) : 10;
|
||||
|
||||
const recentClasses = await db["class-achievements"].find(
|
||||
{
|
||||
game,
|
||||
playtype,
|
||||
},
|
||||
{
|
||||
limit,
|
||||
sort: { timeAchieved: -1 },
|
||||
}
|
||||
);
|
||||
|
||||
const users = await GetUsersWithIDs(recentClasses.map((e) => e.userID));
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Returned ${recentClasses.length} recent classes.`,
|
||||
body: { classes: recentClasses, users },
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// @todo #196 Country Leaderboards?
|
||||
|
||||
router.use("/charts", chartsRouter);
|
||||
router.use("/songs/:songID", songIDRouter);
|
||||
router.use("/folders", foldersRouter);
|
||||
router.use("/tables", tablesRouter);
|
||||
router.use("/scores", scoresRouter);
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user