mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-02 03:48:14 +03:00
Groundwork for USC IR
This commit is contained in:
Vendored
+4
-1
@@ -1,5 +1,6 @@
|
||||
import { Session, SessionData } from "express-session";
|
||||
import { KtchiSessionData } from "../../src/types";
|
||||
import { SYMBOL_KtchiData } from "../../src/constants/ktchi";
|
||||
import { KtchiRequestData, KtchiSessionData } from "../../src/types";
|
||||
|
||||
export {};
|
||||
|
||||
@@ -15,6 +16,8 @@ declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
session: Session & Partial<SessionData>;
|
||||
|
||||
[SYMBOL_KtchiData]?: Partial<KtchiRequestData>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+3
-3
@@ -62,7 +62,7 @@ dependencies:
|
||||
express-session: 1.17.1
|
||||
fast-json-stable-hash: 1.0.2
|
||||
fast-xml-parser: 3.19.0
|
||||
kamaitachi-common: github.com/zkldi/Kamaitachi-common/d93f07f61f44116f709365af61606566765e297e
|
||||
kamaitachi-common: github.com/zkldi/Kamaitachi-common/4f1a7215bfc4fc7ab140f94ae1a8f10dd69129b6
|
||||
mongodb: 3.6.6
|
||||
monk: 7.3.4
|
||||
multer: 1.4.2
|
||||
@@ -4644,8 +4644,8 @@ packages:
|
||||
'@types/yoga-layout': 1.9.2
|
||||
dev: true
|
||||
|
||||
github.com/zkldi/Kamaitachi-common/d93f07f61f44116f709365af61606566765e297e:
|
||||
resolution: {tarball: https://codeload.github.com/zkldi/Kamaitachi-common/tar.gz/d93f07f61f44116f709365af61606566765e297e}
|
||||
github.com/zkldi/Kamaitachi-common/4f1a7215bfc4fc7ab140f94ae1a8f10dd69129b6:
|
||||
resolution: {tarball: https://codeload.github.com/zkldi/Kamaitachi-common/tar.gz/4f1a7215bfc4fc7ab140f94ae1a8f10dd69129b6}
|
||||
name: kamaitachi-common
|
||||
version: 0.1.0
|
||||
dependencies:
|
||||
|
||||
@@ -25,12 +25,13 @@ const router: Router = Router({ mergeParams: true });
|
||||
|
||||
const LAZY_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/u;
|
||||
|
||||
/* istanbul ignore next */
|
||||
const BASE_DOMAIN = process.env.NODE_ENV === "production" ? ".kamaitachi.xyz" : "127.0.0.1";
|
||||
const SHOULD_COOKIES_SECURE = process.env.NODE_ENV === "production";
|
||||
|
||||
/**
|
||||
* Logs in a user.
|
||||
* @name /api/auth/login
|
||||
* @name POST /api/auth/login
|
||||
*/
|
||||
router.post(
|
||||
"/login",
|
||||
@@ -56,6 +57,7 @@ router.post(
|
||||
|
||||
logger.verbose(`Recieved login request with username ${req.body.username} (${req.ip})`);
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
logger.verbose("Validating captcha...");
|
||||
const validCaptcha = await ValidateCaptcha(
|
||||
@@ -152,7 +154,7 @@ router.post(
|
||||
|
||||
/**
|
||||
* Registers a new user.
|
||||
* @name /api/auth/register
|
||||
* @name POST /api/auth/register
|
||||
*/
|
||||
router.post(
|
||||
"/register",
|
||||
@@ -268,7 +270,7 @@ router.post(
|
||||
|
||||
/**
|
||||
* Logs out the requesting user.
|
||||
* @name /api/auth/logout
|
||||
* @name POST /api/auth/logout
|
||||
*/
|
||||
router.post("/logout", RequireLoggedIn, (req, res) => {
|
||||
req.session.destroy(() => 0);
|
||||
|
||||
@@ -42,7 +42,7 @@ const parseMultipartScoredata = (req: Request, res: Response, next: NextFunction
|
||||
|
||||
/**
|
||||
* Import scores from a file. Expects the post request to be multipart, and to provide a scoreData file.
|
||||
* @name /api/import/file
|
||||
* @name POST /api/import/file
|
||||
*/
|
||||
router.post(
|
||||
"/file",
|
||||
|
||||
@@ -119,6 +119,10 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (softID.model === MODEL_INFINITAS_2) {
|
||||
return next(); // allow anything for inf2.
|
||||
}
|
||||
|
||||
if (softID.ext !== EXT_HEROIC_VERSE) {
|
||||
logger.info(
|
||||
`Rejected invalid Software Model ${softID.ext} from user ${
|
||||
@@ -141,6 +145,8 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => {
|
||||
return next();
|
||||
};
|
||||
|
||||
router.use(RequireLoggedIn, ValidateFervidexHeader, ValidateModelHeader);
|
||||
|
||||
/**
|
||||
* Submits all of a users data to Kamaitachi. This data is extremely minimal,
|
||||
* as only a users Lamp and Score are sent. As such, this is not the prefered
|
||||
@@ -149,29 +155,23 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => {
|
||||
*
|
||||
* @name /api/ir/fervidex/profile/submit
|
||||
*/
|
||||
router.post(
|
||||
"/profile/submit",
|
||||
RequireLoggedIn,
|
||||
ValidateFervidexHeader,
|
||||
RequireInf2ModelHeader,
|
||||
async (req, res) => {
|
||||
const userDoc = await GetUserWithIDGuaranteed(req.session.ktchi!.userID);
|
||||
router.post("/profile/submit", RequireInf2ModelHeader, async (req, res) => {
|
||||
const userDoc = await GetUserWithIDGuaranteed(req.session.ktchi!.userID);
|
||||
|
||||
const headers = {
|
||||
// guaranteed to exist because of RequireInf2ModelHeader
|
||||
model: req.header("X-Software-Model")!,
|
||||
};
|
||||
const headers = {
|
||||
// guaranteed to exist because of RequireInf2ModelHeader
|
||||
model: req.header("X-Software-Model")!,
|
||||
};
|
||||
|
||||
const responseData = await ExpressWrappedScoreImportMain(
|
||||
userDoc,
|
||||
false,
|
||||
"ir/fervidex-static",
|
||||
(logger) => ParseFervidexStatic(req.body, headers, logger)
|
||||
);
|
||||
const responseData = await ExpressWrappedScoreImportMain(
|
||||
userDoc,
|
||||
false,
|
||||
"ir/fervidex-static",
|
||||
(logger) => ParseFervidexStatic(req.body, headers, logger)
|
||||
);
|
||||
|
||||
return res.status(responseData.statusCode).json(responseData.body);
|
||||
}
|
||||
);
|
||||
return res.status(responseData.statusCode).json(responseData.body);
|
||||
});
|
||||
|
||||
/**
|
||||
* Submits a single score to Kamaitachi. In contrast to profile/submit, this
|
||||
@@ -180,37 +180,31 @@ router.post(
|
||||
*
|
||||
* @name /api/ir/fervidex/score/submit
|
||||
*/
|
||||
router.post(
|
||||
"/score/submit",
|
||||
RequireLoggedIn,
|
||||
ValidateFervidexHeader,
|
||||
ValidateModelHeader,
|
||||
async (req, res) => {
|
||||
const userDoc = await GetUserWithIDGuaranteed(req.session.ktchi!.userID);
|
||||
router.post("/score/submit", ValidateModelHeader, async (req, res) => {
|
||||
const userDoc = await GetUserWithIDGuaranteed(req.session.ktchi!.userID);
|
||||
|
||||
const model = req.header("X-Software-Model");
|
||||
const model = req.header("X-Software-Model");
|
||||
|
||||
if (!model) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: "No X-Software-Model header provided?",
|
||||
});
|
||||
}
|
||||
|
||||
const headers = {
|
||||
model,
|
||||
};
|
||||
|
||||
const responseData = await ExpressWrappedScoreImportMain(
|
||||
userDoc,
|
||||
true,
|
||||
"ir/fervidex",
|
||||
(logger) => ParseFervidexSingle(req.body, headers, logger)
|
||||
);
|
||||
|
||||
return res.status(responseData.statusCode).json(responseData.body);
|
||||
if (!model) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: "No X-Software-Model header provided?",
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const headers = {
|
||||
model,
|
||||
};
|
||||
|
||||
const responseData = await ExpressWrappedScoreImportMain(
|
||||
userDoc,
|
||||
true,
|
||||
"ir/fervidex",
|
||||
(logger) => ParseFervidexSingle(req.body, headers, logger)
|
||||
);
|
||||
|
||||
return res.status(responseData.statusCode).json(responseData.body);
|
||||
});
|
||||
|
||||
/**
|
||||
* Submits the result of a class to Kamaitachi. This contains the dan played
|
||||
@@ -218,43 +212,35 @@ router.post(
|
||||
*
|
||||
* @name /api/ir/fervidex/class/submit
|
||||
*/
|
||||
router.post(
|
||||
"/class/submit",
|
||||
RequireLoggedIn,
|
||||
ValidateFervidexHeader,
|
||||
ValidateModelHeader,
|
||||
async (req, res) => {
|
||||
if (!req.body.cleared) {
|
||||
return res
|
||||
.status(200)
|
||||
.json({ success: true, description: "No Update Made.", body: {} });
|
||||
}
|
||||
router.post("/class/submit", ValidateModelHeader, async (req, res) => {
|
||||
if (!req.body.cleared) {
|
||||
return res.status(200).json({ success: true, description: "No Update Made.", body: {} });
|
||||
}
|
||||
|
||||
const classVal = FERVIDEX_COURSE_LOOKUP[req.body.course_id];
|
||||
const classVal = FERVIDEX_COURSE_LOOKUP[req.body.course_id];
|
||||
|
||||
if (!classVal) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid courseID of ${req.body.course_id}.`,
|
||||
});
|
||||
}
|
||||
|
||||
// is 0 or 1.
|
||||
const playtype: Playtypes["iidx"] = req.body.playstyle ? "SP" : "DP";
|
||||
|
||||
const r = await UpdateClassIfGreater(
|
||||
req.session.ktchi!.userID,
|
||||
"iidx",
|
||||
playtype,
|
||||
"dan",
|
||||
classVal
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: r === false ? "Dan unchanged." : "Dan changed!",
|
||||
if (!classVal) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid courseID of ${req.body.course_id}.`,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// is 0 or 1.
|
||||
const playtype: Playtypes["iidx"] = req.body.playstyle ? "SP" : "DP";
|
||||
|
||||
const r = await UpdateClassIfGreater(
|
||||
req.session.ktchi!.userID,
|
||||
"iidx",
|
||||
playtype,
|
||||
"dan",
|
||||
classVal
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: r === false ? "Dan unchanged." : "Dan changed!",
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -3,6 +3,7 @@ import barbatosIR from "./barbatos/barbatos";
|
||||
import chunitachiIR from "./chunitachi/chunitachi";
|
||||
import directManualIR from "./direct-manual/direct-manual";
|
||||
import fervidexIR from "./fervidex/fervidex";
|
||||
import uscIR from "./usc/usc";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
@@ -10,5 +11,6 @@ router.use("/barbatos", barbatosIR);
|
||||
router.use("/chunitachi", chunitachiIR);
|
||||
router.use("/direct-manual", directManualIR);
|
||||
router.use("/fervidex", fervidexIR);
|
||||
router.use("/usc", uscIR);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { integer, PBScoreDocument, ScoreDocument } from "kamaitachi-common";
|
||||
import CreateLogCtx from "../../../common/logger";
|
||||
import db from "../../../db/db";
|
||||
|
||||
const logger = CreateLogCtx("ir/usc/common.ts");
|
||||
|
||||
export interface USCServerScore {
|
||||
score: integer;
|
||||
lamp: 0 | 1 | 2 | 3 | 4 | 5;
|
||||
timestamp: integer;
|
||||
crit: integer;
|
||||
near: integer;
|
||||
error: integer;
|
||||
ranking: integer;
|
||||
gaugeMod: "NORMAL" | "HARD";
|
||||
noteMod: "NORMAL" | "MIRROR" | "RANDOM" | "MIR-RAN";
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface USCClientScore {
|
||||
score: integer;
|
||||
gameflags: integer;
|
||||
gauge: number;
|
||||
timestamp: integer;
|
||||
crit: integer;
|
||||
near: integer;
|
||||
error: integer;
|
||||
options: {
|
||||
gaugeType: integer;
|
||||
gaugeOpt: integer;
|
||||
mirror: boolean;
|
||||
random: boolean;
|
||||
autoFlags: integer; //???
|
||||
};
|
||||
windows: {
|
||||
perfect: number;
|
||||
good: number;
|
||||
hold: number;
|
||||
miss: number;
|
||||
slam: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface USCClientChart {
|
||||
chartHash: string;
|
||||
artist: string;
|
||||
title: string;
|
||||
level: integer;
|
||||
difficulty: 0 | 1 | 2 | 3;
|
||||
effector: string;
|
||||
illustrator: string;
|
||||
bpm: string;
|
||||
}
|
||||
|
||||
export const KT_LAMP_TO_USC: Record<
|
||||
PBScoreDocument<"usc:Single">["scoreData"]["lamp"],
|
||||
USCServerScore["lamp"]
|
||||
> = {
|
||||
// we don't do NO PLAY, so its not handled.
|
||||
FAILED: 1,
|
||||
CLEAR: 2,
|
||||
"EXCESSIVE CLEAR": 3,
|
||||
"ULTIMATE CHAIN": 4,
|
||||
"PERFECT ULTIMATE CHAIN": 5,
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a Kamaitachi Score to the ServerScoreDocument
|
||||
* as specified in the USCIR spec. This function silently
|
||||
* returns sentinel values in the case that certain
|
||||
* fields are null.
|
||||
*/
|
||||
export async function KtchiScoreToServerScore(
|
||||
ktchiScore: PBScoreDocument<"usc:Single">
|
||||
): Promise<USCServerScore> {
|
||||
const userDoc = await db.users.findOne(
|
||||
{
|
||||
id: ktchiScore.userID,
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
username: 1,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!userDoc) {
|
||||
logger.severe(
|
||||
`User ${ktchiScore.userID} from PB on chart ${ktchiScore.chartID} has no user document?`
|
||||
);
|
||||
throw new Error(
|
||||
`User ${ktchiScore.userID} from PB on chart ${ktchiScore.chartID} has no user document?`
|
||||
);
|
||||
}
|
||||
|
||||
const scorePB = (await db.scores.findOne({
|
||||
scoreID: ktchiScore.composedFrom.scorePB,
|
||||
})) as ScoreDocument<"usc:Single"> | null;
|
||||
|
||||
if (!scorePB) {
|
||||
logger.severe(
|
||||
`Score ${ktchiScore.composedFrom.scorePB} does not exist, but is referenced in ${ktchiScore.userID}'s PBDoc on ${ktchiScore.chartID}?`
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`User ${ktchiScore.userID} from PB on chart ${ktchiScore.chartID} has no user document?`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
score: ktchiScore.scoreData.score,
|
||||
timestamp: Math.floor((ktchiScore.timeAchieved ?? 0) / 1000),
|
||||
crit: ktchiScore.scoreData.hitData.critical ?? 0,
|
||||
near: ktchiScore.scoreData.hitData.near ?? 0,
|
||||
error: ktchiScore.scoreData.hitData.miss ?? 0,
|
||||
ranking: ktchiScore.rankingData.rank,
|
||||
lamp: KT_LAMP_TO_USC[ktchiScore.scoreData.lamp],
|
||||
username: userDoc.username,
|
||||
noteMod: scorePB.scoreMeta.noteMod ?? "NORMAL",
|
||||
gaugeMod: scorePB.scoreMeta.gaugeMod ?? "NORMAL",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import { Router, RequestHandler } from "express";
|
||||
import { FindChartOnSHA256 } from "../../../common/database-lookup/chart";
|
||||
import { SYMBOL_KtchiData } from "../../../constants/ktchi";
|
||||
import db from "../../../db/db";
|
||||
import { ChartDocument, PBScoreDocument } from "kamaitachi-common";
|
||||
import { AssertStrAsPositiveNonZeroInt } from "../../../score-import/framework/common/string-asserts";
|
||||
import CreateLogCtx from "../../../common/logger";
|
||||
import { KtchiScoreToServerScore } from "./common";
|
||||
|
||||
const logger = CreateLogCtx("usc.ts");
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
const STATUS_CODES = {
|
||||
UNAUTH: 41,
|
||||
CHART_REFUSE: 42,
|
||||
FORBIDDEN: 43,
|
||||
NOT_FOUND: 44,
|
||||
SERVER_ERROR: 50,
|
||||
SUCCESS: 20,
|
||||
BAD_REQ: 40,
|
||||
};
|
||||
|
||||
// This is an implementation of the USCIR spec as per https://uscir.readthedocs.io.
|
||||
// This specification always returns 200 OK, regardless of whether the result was okay
|
||||
// as the HTTP code is used to determine whether the server received the request properly,
|
||||
// rather than the result of the request.
|
||||
|
||||
const ValidateUSCRequest: RequestHandler = async (req, res, next) => {
|
||||
const token = req.header("Authorization");
|
||||
|
||||
if (!token) {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.UNAUTH,
|
||||
description: "No auth token provided.",
|
||||
});
|
||||
}
|
||||
|
||||
const splitToken = token.split(" ");
|
||||
|
||||
if (splitToken.length !== 2 || splitToken[0] !== "Bearer") {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.BAD_REQ,
|
||||
description: "Invalid Authorization Header. Expected Bearer <token>",
|
||||
});
|
||||
}
|
||||
|
||||
const uscAuthDoc = await db["usc-auth-tokens"].findOne({
|
||||
token: splitToken[1],
|
||||
});
|
||||
|
||||
if (!uscAuthDoc) {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.UNAUTH,
|
||||
description: "Unauthorized.",
|
||||
});
|
||||
}
|
||||
|
||||
req[SYMBOL_KtchiData] = { uscAuthDoc };
|
||||
};
|
||||
|
||||
router.use(ValidateUSCRequest);
|
||||
|
||||
/**
|
||||
* Used to check your connection to the server, and receive some basic information.
|
||||
* https://uscir.readthedocs.io/en/latest/endpoints/heartbeat.html
|
||||
* @name GET /api/ir/usc
|
||||
*/
|
||||
router.get("/", (req, res) =>
|
||||
res.status(200).json({
|
||||
statusCode: STATUS_CODES.SUCCESS,
|
||||
description: "IR Request Successful.",
|
||||
body: {
|
||||
serverTime: Math.floor(Date.now() / 1000),
|
||||
serverName: "Kamaitachi BLACK",
|
||||
irVersion: "0.3.0-a",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const RetrieveChart: RequestHandler = async (req, res, next) => {
|
||||
const chart = await FindChartOnSHA256("usc", req.params.chartHash);
|
||||
|
||||
req[SYMBOL_KtchiData]!.uscChartDoc = (chart ?? undefined) as
|
||||
| ChartDocument<"usc:Single">
|
||||
| undefined;
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
const MAX_N = 10;
|
||||
|
||||
/**
|
||||
* Used to check if the server will accept a score for a given chart in advance of submitting it.
|
||||
* https://uscir.readthedocs.io/en/latest/endpoints/chart-charthash.html
|
||||
* @name GET /api/ir/usc/charts/:chartHash
|
||||
*/
|
||||
router.get("/charts/:chartHash", RetrieveChart, (req, res) => {
|
||||
const chart = req[SYMBOL_KtchiData]!.uscChartDoc;
|
||||
|
||||
if (!chart) {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.CHART_REFUSE,
|
||||
description: "This chart is not tracked, and will not be accepted.",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.SUCCESS,
|
||||
description: "This chart is tracked by the IR.",
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Used to retrieve the current server record for the chart with the specified hash.
|
||||
* https://uscir.readthedocs.io/en/latest/endpoints/record.html
|
||||
* @name GET /api/ir/usc/charts/:chartHash/record
|
||||
*/
|
||||
router.get("/charts/:chartHash/record", RetrieveChart, async (req, res) => {
|
||||
const chart = req[SYMBOL_KtchiData]!.uscChartDoc;
|
||||
|
||||
// spec ambigious here
|
||||
|
||||
if (!chart) {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.CHART_REFUSE,
|
||||
description: "This IR is not currently tracking this chart.",
|
||||
});
|
||||
}
|
||||
|
||||
const serverRecord = (await db["score-pbs"].findOne({
|
||||
chartID: chart.chartID,
|
||||
"rankingData.rank": 1,
|
||||
})) as PBScoreDocument<"usc:Single"> | null;
|
||||
|
||||
if (!serverRecord) {
|
||||
// @todo See uscir-spec #1
|
||||
return res.status(200).json({
|
||||
success: STATUS_CODES.SERVER_ERROR,
|
||||
description: "TODO",
|
||||
});
|
||||
}
|
||||
|
||||
const serverScore = await KtchiScoreToServerScore(serverRecord);
|
||||
|
||||
return res.status(200).json({
|
||||
success: STATUS_CODES.SUCCESS,
|
||||
description: "Retrieved score.",
|
||||
body: serverScore,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Used to retrieve some particular useful subset of the scores from the server.
|
||||
* https://uscir.readthedocs.io/en/latest/endpoints/leaderboard.html
|
||||
* @name GET /api/ir/usc/charts/:chartHash/leaderboard
|
||||
*/
|
||||
router.get("/charts/:chartHash/leaderboard", RetrieveChart, async (req, res) => {
|
||||
const chart = req[SYMBOL_KtchiData]!.uscChartDoc!;
|
||||
|
||||
if (!(typeof req.query.mode === "string" && ["best", "rivals"].includes(req.query.mode))) {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.BAD_REQ,
|
||||
description: `Invalid 'mode' param - expected 'best' or 'rivals'.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof req.query.n !== "string") {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.BAD_REQ,
|
||||
description: `Invalid 'n' param - expected a positive non-zero integer less than or equal to ${MAX_N}.`,
|
||||
});
|
||||
}
|
||||
|
||||
let n;
|
||||
|
||||
try {
|
||||
n = AssertStrAsPositiveNonZeroInt(req.query.n, "Invalid 'N' param.");
|
||||
} catch (err) {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.BAD_REQ,
|
||||
description: `Invalid 'n' param - expected a positive non-zero integer less than or equal to ${MAX_N}.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (n >= MAX_N) {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.BAD_REQ,
|
||||
description: `Invalid 'n' param - expected a positive non-zero integer less than or equal to ${MAX_N}.`,
|
||||
});
|
||||
}
|
||||
|
||||
const mode = req.query.mode as "best" | "rivals";
|
||||
|
||||
if (mode === "rivals") {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.BAD_REQ,
|
||||
description: "This is currently unsupported.",
|
||||
});
|
||||
}
|
||||
|
||||
const bestScores = (await db["score-pbs"].find(
|
||||
{
|
||||
chartID: chart.chartID,
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
"scoreData.perecent": -1,
|
||||
},
|
||||
limit: n,
|
||||
}
|
||||
)) as PBScoreDocument<"usc:Single">[];
|
||||
|
||||
const serverScores = await Promise.all(bestScores.map(KtchiScoreToServerScore));
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.SUCCESS,
|
||||
description: `Returned ${serverScores.length} scores.`,
|
||||
body: serverScores,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Sends a score to the server.
|
||||
* https://uscir.readthedocs.io/en/latest/endpoints/score-submit.html
|
||||
* @name POST /api/ir/usc/scores
|
||||
*/
|
||||
router.post("/scores", async (req, res) => {});
|
||||
|
||||
export default router;
|
||||
@@ -199,3 +199,12 @@ export function FindSDVXChartOnInGameIDVersion(
|
||||
versions: version,
|
||||
});
|
||||
}
|
||||
|
||||
export function FindChartOnSHA256(game: Game, hash: string) {
|
||||
// @todo #112 Reject requests to this for things that
|
||||
// dont have sha256 props.
|
||||
|
||||
return db.charts[game].findOne({
|
||||
"data.hashSHA256": hash,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const SYMBOL_KtchiData = Symbol("ktchi-request-patch");
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
TierlistParent,
|
||||
InviteCodeDocument,
|
||||
TierlistDataDocument,
|
||||
USCAuthDocument,
|
||||
MilestoneDocument,
|
||||
NotificationDocument,
|
||||
FolderChartLookup,
|
||||
@@ -118,6 +119,7 @@ const db = {
|
||||
"iidx-eam-scores": monkDB.get<IIDXEamusementScoreDocument>("iidx-eam-scores"),
|
||||
"game-stats": monkDB.get<UserGameStats>("game-stats"),
|
||||
"kai-auth-tokens": monkDB.get<KaiAuthDocument>("kai-auth-tokens"),
|
||||
"usc-auth-tokens": monkDB.get<USCAuthDocument>("usc-auth-tokens"),
|
||||
};
|
||||
|
||||
export default db;
|
||||
|
||||
@@ -56,7 +56,7 @@ app.use("/api", apiRouter);
|
||||
|
||||
/**
|
||||
* If any user gets to this point, we send them index.html and let react router do the routing (and 404ing)
|
||||
* @name *
|
||||
* @name ALL *
|
||||
*/
|
||||
app.get("*", (req, res) => res.status(200).send("todo"));
|
||||
app.all("*", (req, res) =>
|
||||
|
||||
@@ -5,7 +5,9 @@ import {
|
||||
integer,
|
||||
ScoreDocument,
|
||||
AnySongDocument,
|
||||
USCAuthDocument,
|
||||
MongoDBDocument,
|
||||
ChartDocument,
|
||||
Game,
|
||||
Playtypes,
|
||||
} from "kamaitachi-common";
|
||||
@@ -185,3 +187,11 @@ export interface ImportTypeContextMap {
|
||||
* Clarity type for empty objects - such as in context.
|
||||
*/
|
||||
export type EmptyObject = Record<string, never>;
|
||||
|
||||
/**
|
||||
* Data that may be monkey-patched onto req.ktchi. This holds things such as middleware results.
|
||||
*/
|
||||
export interface KtchiRequestData {
|
||||
uscAuthDoc?: USCAuthDocument;
|
||||
uscChartDoc?: ChartDocument<"usc:Single">;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user