mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-06 05:48:00 +03:00
experimental: massive hollis changes
This commit is contained in:
@@ -50,19 +50,28 @@ export function SendEmail(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!transporter) {
|
||||
if (!transporter || !ServerConfig.EMAIL_CONFIG) {
|
||||
logger.debug(`Stubbed out SendEmail as no EMAIL_CONFIG was set.`);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.verbose(`Sending email to ${to}.`);
|
||||
|
||||
return transporter.sendMail({
|
||||
from: ServerConfig.EMAIL_CONFIG!.FROM,
|
||||
to,
|
||||
subject,
|
||||
html: htmlContent,
|
||||
text: textContent,
|
||||
dkim: ServerConfig.EMAIL_CONFIG?.DKIM,
|
||||
});
|
||||
return transporter
|
||||
.sendMail({
|
||||
from: ServerConfig.EMAIL_CONFIG.FROM,
|
||||
to,
|
||||
subject,
|
||||
html: htmlContent,
|
||||
text: textContent,
|
||||
dkim: ServerConfig.EMAIL_CONFIG.DKIM,
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
logger.info(`Failed to send email to ${to}.`, {
|
||||
err,
|
||||
subject,
|
||||
textContent,
|
||||
htmlContent,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ const LOG_LEVEL = ServerConfig.LOGGER_CONFIG.LOG_LEVEL;
|
||||
t.test("Logger Tests", (t) => {
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
if (!Transports[0]) {
|
||||
throw new Error(`No transports were defined? Can't perform logger tests.`)
|
||||
}
|
||||
|
||||
Transports[0].level = "debug"; // lol
|
||||
|
||||
logger.debug("Debug Message Test");
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
import type { ImportTypeContextMap, ImportTypeDataMap } from "../../import-types/common/types";
|
||||
import type { ImportTypes } from "tachi-common";
|
||||
|
||||
export class ConverterFailure {
|
||||
export class ConverterFailure extends Error {
|
||||
message: string;
|
||||
|
||||
constructor(message: string) {
|
||||
super();
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export function GetGradeFromPercent<I extends IDStrings = IDStrings>(
|
||||
}
|
||||
|
||||
// (hey, this for loop is backwards!)
|
||||
for (let i = boundaries.length; i >= 0; i--) {
|
||||
for (let i = boundaries.length - 1; i >= 0; i--) {
|
||||
if (percent + Number.EPSILON >= NotNullish(boundaries[i])) {
|
||||
return grades[i] as Grades[I];
|
||||
}
|
||||
|
||||
@@ -25,66 +25,67 @@ function astrp(v: string) {
|
||||
t.test("#AssertStrAsPositiveInt", (t) => {
|
||||
t.strictSame(
|
||||
astr("---"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- ---.)`),
|
||||
"Should reject non-int input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("1.4"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- 1.4.)`),
|
||||
"Should reject float input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("NaN"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- NaN.)`),
|
||||
"Should reject NaN"
|
||||
);
|
||||
t.strictSame(
|
||||
astr("-0.3"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- -0.3.)`),
|
||||
"Should reject negative float input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("0xFF"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- 0xFF.)`),
|
||||
"Should reject hex input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("0b11"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- 0b11.)`),
|
||||
"Should reject binary input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("0o77"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- 0o77.)`),
|
||||
"Should reject oct input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("--1"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- --1.)`),
|
||||
"Should reject double neg input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("12f"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- 12f.)`),
|
||||
"Should reject valid parseInt but invalid Number() input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr(`${Number.MAX_SAFE_INTEGER.toString()}000`),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
// @warn Counterintuitive errmsg here. It is an int!
|
||||
new InvalidScoreFailure(`err (Not an integer -- ${Number.MAX_SAFE_INTEGER.toString()}000.)`),
|
||||
"Should reject numbers > max_safe_integer."
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astr("-1"),
|
||||
new InvalidScoreFailure(`err (Was negative.)`),
|
||||
new InvalidScoreFailure(`err (Was negative -- -1.)`),
|
||||
"Should reject negative integer input"
|
||||
);
|
||||
|
||||
@@ -98,78 +99,79 @@ t.test("#AssertStrAsPositiveInt", (t) => {
|
||||
t.test("#AssertStrAsPositiveNonZeroInt", (t) => {
|
||||
t.strictSame(
|
||||
astrp("---"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- ---.)`),
|
||||
"Should reject non-int input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("1.4"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- 1.4.)`),
|
||||
"Should reject float input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("NaN"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- NaN.)`),
|
||||
"Should reject NaN"
|
||||
);
|
||||
t.strictSame(
|
||||
astrp("-0.3"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- -0.3.)`),
|
||||
"Should reject negative float input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("0xFF"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- 0xFF.)`),
|
||||
"Should reject hex input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("0b11"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- 0b11.)`),
|
||||
"Should reject binary input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("0o77"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- 0o77.)`),
|
||||
"Should reject oct input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("--1"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- --1.)`),
|
||||
"Should reject double neg input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("12f"),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
new InvalidScoreFailure(`err (Not an integer -- 12f.)`),
|
||||
"Should reject valid parseInt but invalid Number() input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp(`${Number.MAX_SAFE_INTEGER.toString()}000`),
|
||||
new InvalidScoreFailure(`err (Not an integer.)`),
|
||||
// @warn Counterintuitive errmsg here. It is an int!
|
||||
new InvalidScoreFailure(`err (Not an integer -- ${Number.MAX_SAFE_INTEGER.toString()}000.)`),
|
||||
"Should reject numbers > max_safe_integer."
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("-1"),
|
||||
new InvalidScoreFailure(`err (Was negative or zero.)`),
|
||||
new InvalidScoreFailure(`err (Was negative or zero -- -1.)`),
|
||||
"Should reject negative integer input"
|
||||
);
|
||||
|
||||
1
|
||||
t.strictSame(
|
||||
astrp("-0"),
|
||||
new InvalidScoreFailure(`err (Was negative or zero.)`),
|
||||
new InvalidScoreFailure(`err (Was negative or zero -- 0.)`),
|
||||
"Should reject negative 0 input"
|
||||
);
|
||||
|
||||
t.strictSame(
|
||||
astrp("0"),
|
||||
new InvalidScoreFailure(`err (Was negative or zero.)`),
|
||||
new InvalidScoreFailure(`err (Was negative or zero -- 0.)`),
|
||||
"Should reject 0 input"
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import t from "tap";
|
||||
import db from "external/mongo/db";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { GetRelevantFolderGoals, GetRelevantGoals, UpdateGoalsForUser, ProcessGoal } from "./goals";
|
||||
import { GoalDocument, GoalSubscriptionDocument } from "tachi-common";
|
||||
import { ChartDocument, GoalDocument, GoalSubscriptionDocument, SongDocument } from "tachi-common";
|
||||
import { CreateFolderChartLookup } from "utils/folder";
|
||||
import {
|
||||
GetKTDataJSON,
|
||||
@@ -93,8 +93,8 @@ t.test("#GetRelevantGoals", (t) => {
|
||||
// use the real data so we have enough charts loaded for this to test properly.
|
||||
await db.songs.iidx.remove({});
|
||||
await db.charts.iidx.remove({});
|
||||
await db.charts.iidx.insert(GetKTDataJSON("./tachi/tachi-charts-iidx.json"));
|
||||
await db.songs.iidx.insert(GetKTDataJSON("./tachi/tachi-songs-iidx.json"));
|
||||
await db.charts.iidx.insert(GetKTDataJSON("./tachi/tachi-charts-iidx.json") as ChartDocument<"iidx:SP"|"iidx:DP">[]);
|
||||
await db.songs.iidx.insert(GetKTDataJSON("./tachi/tachi-songs-iidx.json") as SongDocument<"iidx">[]);
|
||||
|
||||
const lotsOfCharts = await db.charts.iidx.find({}, { limit: 20 });
|
||||
const goals: GoalDocument[] = lotsOfCharts.map((e) => ({
|
||||
|
||||
@@ -133,7 +133,7 @@ t.test("#ReprocessOrphan", (t) => {
|
||||
{
|
||||
success: true,
|
||||
type: "ScoreImported",
|
||||
message: null,
|
||||
message: "Imported score R7a3a2b04bd4882ec06c198d78297fe3d56561502c7b134c067214bbfdf4f1602.",
|
||||
content: {
|
||||
score: {
|
||||
game: "iidx",
|
||||
|
||||
@@ -45,7 +45,7 @@ t.test("#CreateSessions", (t) => {
|
||||
userID: 1,
|
||||
importType: "ir/direct-manual",
|
||||
// name: "adjective1 adjective2 noun1",
|
||||
sessionID: res[0].sessionID,
|
||||
sessionID: res[0]?.sessionID,
|
||||
desc: null,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
@@ -137,7 +137,7 @@ t.test("#LoadScoresIntoSessions", (t) => {
|
||||
|
||||
t.equal(sessions.length, 1);
|
||||
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
t.strictSame(sessions[0]?.scoreInfo, [
|
||||
{
|
||||
scoreID: "SCORE_ID_1",
|
||||
isNewScore: true,
|
||||
@@ -183,7 +183,7 @@ t.test("#LoadScoresIntoSessions", (t) => {
|
||||
|
||||
t.equal(sessions.length, 1);
|
||||
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
t.strictSame(sessions[0]?.scoreInfo, [
|
||||
{
|
||||
scoreID: "SCORE_ID_2",
|
||||
isNewScore: true,
|
||||
@@ -246,7 +246,7 @@ t.test("#LoadScoresIntoSessions", (t) => {
|
||||
|
||||
t.equal(sessions.length, 2);
|
||||
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
t.strictSame(sessions[0]?.scoreInfo, [
|
||||
{
|
||||
scoreID: "SCORE_ID_1",
|
||||
isNewScore: true,
|
||||
@@ -260,7 +260,7 @@ t.test("#LoadScoresIntoSessions", (t) => {
|
||||
isNewScore: true,
|
||||
},
|
||||
]);
|
||||
t.strictSame(sessions[1].scoreInfo, [
|
||||
t.strictSame(sessions[1]?.scoreInfo, [
|
||||
{
|
||||
scoreID: "SCORE_ID_4",
|
||||
isNewScore: true,
|
||||
@@ -317,7 +317,7 @@ t.test("#LoadScoresIntoSessions", (t) => {
|
||||
|
||||
t.equal(sessions.length, 1);
|
||||
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
t.strictSame(sessions[0]?.scoreInfo, [
|
||||
{
|
||||
scoreID: "EXAMPLE_SCORE_ID",
|
||||
isNewScore: true,
|
||||
@@ -336,7 +336,7 @@ t.test("#LoadScoresIntoSessions", (t) => {
|
||||
},
|
||||
]);
|
||||
|
||||
t.equal(sessions[0].timeEnded, start + 2000);
|
||||
t.equal(sessions[0]?.timeEnded, start + 2000);
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -380,7 +380,7 @@ t.test("#LoadScoresIntoSessions", (t) => {
|
||||
|
||||
t.equal(sessions.length, 1);
|
||||
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
t.strictSame(sessions[0]?.scoreInfo, [
|
||||
{
|
||||
scoreID: "EXAMPLE_SCORE_ID",
|
||||
isNewScore: true,
|
||||
@@ -399,7 +399,7 @@ t.test("#LoadScoresIntoSessions", (t) => {
|
||||
},
|
||||
]);
|
||||
|
||||
t.equal(sessions[0].timeStarted, start - 2000);
|
||||
t.equal(sessions[0]?.timeStarted, start - 2000);
|
||||
|
||||
t.end();
|
||||
});
|
||||
@@ -427,7 +427,7 @@ t.test("#LoadScoresIntoSessions", (t) => {
|
||||
|
||||
t.equal(sessions.length, 1);
|
||||
|
||||
t.strictSame(sessions[0].scoreInfo, [
|
||||
t.strictSame(sessions[0]?.scoreInfo, [
|
||||
{
|
||||
scoreID: "TESTING_SCORE_ID",
|
||||
isNewScore: false,
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#CalculateGitadoraColour", (t) => {
|
||||
t.strictSame(
|
||||
CalculateGitadoraColour("gitadora", "Gita", 1, { skill: 1500 }, logger),
|
||||
CalculateGitadoraColour("gitadora", "Gita", 1, { skill: 1500 }),
|
||||
{
|
||||
colour: GitadoraColours.ORANGE_GRADIENT,
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import deepmerge from "deepmerge";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { BatchManualScore, Game } from "tachi-common";
|
||||
import { BatchManualScore, ChartDocument, Game, SongDocument } from "tachi-common";
|
||||
import t from "tap";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { GetKTDataJSON, Testing511Song, Testing511SPA } from "test-utils/test-data";
|
||||
import { BMSGazerChart, BMSGazerSong, GetKTDataJSON, Testing511Song, Testing511SPA } from "test-utils/test-data";
|
||||
import { EscapeStringRegexp } from "utils/misc";
|
||||
import { InvalidScoreFailure } from "../../../framework/common/converter-failures";
|
||||
import { ConverterBatchManual, ResolveChartFromSong, ResolveMatchTypeToKTData } from "./converter";
|
||||
@@ -104,9 +104,6 @@ t.test("#ResolveMatchTypeToKTData", (t) => {
|
||||
const GAZER17MD5 = "38616b85332037cc12924f2ae2840262";
|
||||
const GAZER17SHA256 = "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d";
|
||||
|
||||
const gazerSong = GetKTDataJSON("./tachi/bms-gazer-song.json");
|
||||
const gazerChart = GetKTDataJSON("./tachi/bms-gazer-chart.json");
|
||||
|
||||
const bmsContext: BatchManualContext = deepmerge(context, { game: "bms", playtype: "7K" });
|
||||
|
||||
const resMD5 = await ResolveMatchTypeToKTData(
|
||||
@@ -121,7 +118,7 @@ t.test("#ResolveMatchTypeToKTData", (t) => {
|
||||
|
||||
t.hasStrict(
|
||||
resMD5,
|
||||
{ song: gazerSong, chart: gazerChart },
|
||||
{ song: BMSGazerSong, chart: BMSGazerChart },
|
||||
"Should return the right song and chart."
|
||||
);
|
||||
|
||||
@@ -137,7 +134,7 @@ t.test("#ResolveMatchTypeToKTData", (t) => {
|
||||
|
||||
t.hasStrict(
|
||||
resSHA256,
|
||||
{ song: gazerSong, chart: gazerChart },
|
||||
{ song: BMSGazerSong, chart: BMSGazerChart },
|
||||
"Should return the right song and chart."
|
||||
);
|
||||
|
||||
|
||||
@@ -219,7 +219,7 @@ t.test("#ParseEamusementCSV", (t) => {
|
||||
|
||||
t.throws(
|
||||
() => GenericParseEamIIDXCSV(validSPFile, {}, "e-amusement", logger),
|
||||
new ScoreImportFatalError(400, "Invalid playtype of Nothing given.")
|
||||
new ScoreImportFatalError(400, "Invalid playtype of undefined given.")
|
||||
);
|
||||
|
||||
let { context } = GenericParseEamIIDXCSV(
|
||||
|
||||
@@ -8,7 +8,14 @@ import { SDVXEamusementCSVData } from "./types";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
const parsedScore = GetKTDataJSON("./eamusement-sdvx-csv/parsed-data.json");
|
||||
const parsedScore = {
|
||||
"title": "ALBIDA Powerless Mix",
|
||||
"difficulty": "ADVANCED",
|
||||
"level": "10",
|
||||
"lamp": "EXCESSIVE COMPLETE",
|
||||
"score": "9310699",
|
||||
"exscore": "0"
|
||||
};
|
||||
|
||||
t.test("#ConvertEamSDVXCSV", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
@@ -16,12 +16,21 @@ const logger = CreateLogCtx(__filename);
|
||||
|
||||
t.test("#ConvertFileMerIIDX", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
const MerScore = {
|
||||
"music_id": 1000,
|
||||
"play_type": "SINGLE",
|
||||
"diff_type": "ANOTHER",
|
||||
"score": 1000,
|
||||
"miss_count": 21,
|
||||
"clear_type": "CLEAR",
|
||||
"update_time": "2021-03-24 07:15:22"
|
||||
} as const;
|
||||
|
||||
function merc(g: Partial<MerScore> = {}) {
|
||||
return ConvertFileMerIIDX(deepmerge(MerScore, g), {}, "file/mer-iidx", logger);
|
||||
}
|
||||
|
||||
const MerScore = GetKTDataJSON("./mer/merscore.json");
|
||||
|
||||
t.test("Valid Conversion", async (t) => {
|
||||
const res = await ConvertFileMerIIDX(MerScore, {}, "file/mer-iidx", logger);
|
||||
|
||||
@@ -17,7 +17,7 @@ t.test("#ParseMerIIDX", (t) => {
|
||||
|
||||
function mrfj(obj: unknown) {
|
||||
return ParseMerIIDX(
|
||||
MockMulterFile(Buffer.from(JSON.stringify(obj)), "buffer.json"),
|
||||
MockMulterFile(Buffer.from(JSON.stringify([obj])), "buffer.json"),
|
||||
{},
|
||||
logger
|
||||
);
|
||||
@@ -75,7 +75,26 @@ t.test("#ParseMerIIDX", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
const baseScore = GetKTDataJSON("./mer/base.json");
|
||||
const baseScore =
|
||||
{
|
||||
"music_id": 3007,
|
||||
"version_id": 3,
|
||||
"version_name": "3rd style",
|
||||
"music_name": "Presto",
|
||||
"play_type": "SINGLE",
|
||||
"diff_type": "HYPER",
|
||||
"score": 566,
|
||||
"score_percent": 56.8273,
|
||||
"miss_count": 46,
|
||||
"grade": "B",
|
||||
"clear_type": "NO PLAY",
|
||||
"clear_type_id": 0,
|
||||
"level": 8,
|
||||
"note": 498,
|
||||
"play_count": 6,
|
||||
"update_time": "2019-06-01 19:56:59",
|
||||
"score_diff_str": "B+12"
|
||||
};
|
||||
|
||||
t.test("Should throw on invalid play_type", (t) => {
|
||||
t.throws(() => mrfj(deepmerge(baseScore, { play_type: "INVALID" })));
|
||||
|
||||
@@ -5,6 +5,7 @@ import ResetDBState from "test-utils/resets";
|
||||
import {
|
||||
GetKTDataJSON,
|
||||
LoadTachiIIDXData,
|
||||
MockParsedS3Score,
|
||||
Testing511Song,
|
||||
Testing511SPA,
|
||||
} from "test-utils/test-data";
|
||||
@@ -20,11 +21,12 @@ function cfile(data: S3Score) {
|
||||
t.test("#ConvertFileS3", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
t.beforeEach(() => {
|
||||
delete BaseS3Score._id; // just incase
|
||||
// @ts-expect-error hacky just-incase.
|
||||
delete MockParsedS3Score._id;
|
||||
});
|
||||
|
||||
function mfile(merge: Partial<S3Score>) {
|
||||
return cfile(deepmerge(BaseS3Score, merge));
|
||||
return cfile(deepmerge(MockParsedS3Score, merge));
|
||||
}
|
||||
|
||||
const dryScore = {
|
||||
@@ -51,10 +53,9 @@ t.test("#ConvertFileS3", (t) => {
|
||||
// timeAchieved: 1287460462000,
|
||||
};
|
||||
|
||||
const BaseS3Score = GetKTDataJSON("./s3/s3score.json");
|
||||
|
||||
t.test("Should import a valid S3 score", async (t) => {
|
||||
const res = await cfile(BaseS3Score);
|
||||
const res = await cfile(MockParsedS3Score);
|
||||
|
||||
t.hasStrict(
|
||||
res,
|
||||
|
||||
@@ -3,7 +3,7 @@ import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { barbScore } from "test-utils/test-data";
|
||||
import { MockBarbatosScore } from "test-utils/test-data";
|
||||
import { ConverterIRBarbatos } from "./converter";
|
||||
import { BarbatosScore } from "./types";
|
||||
|
||||
@@ -41,7 +41,7 @@ t.test("#ConverterIRBarbatos", (t) => {
|
||||
|
||||
t.test("Should convert a BarbatosScore into a Dry Score", async (t) => {
|
||||
const res = await ConverterIRBarbatos(
|
||||
barbScore,
|
||||
MockBarbatosScore,
|
||||
{ timeReceived: 10 },
|
||||
"ir/barbatos",
|
||||
logger
|
||||
@@ -86,7 +86,7 @@ t.test("#ConverterIRBarbatos", (t) => {
|
||||
t.rejects(
|
||||
() =>
|
||||
ConverterIRBarbatos(
|
||||
deepmerge(barbScore, { song_id: 1000 }) as BarbatosScore,
|
||||
deepmerge(MockBarbatosScore, { song_id: 1000 }) as BarbatosScore,
|
||||
{ timeReceived: 10 },
|
||||
"ir/barbatos",
|
||||
logger
|
||||
@@ -103,7 +103,7 @@ t.test("#ConverterIRBarbatos", (t) => {
|
||||
await db.songs.sdvx.remove({ id: 1 }); // force a song-chart desync
|
||||
|
||||
t.rejects(
|
||||
() => ConverterIRBarbatos(barbScore, { timeReceived: 10 }, "ir/barbatos", logger),
|
||||
() => ConverterIRBarbatos(MockBarbatosScore, { timeReceived: 10 }, "ir/barbatos", logger),
|
||||
{
|
||||
message: /Song 1 \(sdvx\) has no parent song/u,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { barbScore } from "test-utils/test-data";
|
||||
import { MockBarbatosScore } from "test-utils/test-data";
|
||||
import { ParseBarbatosSingle } from "./parser";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
@@ -10,12 +10,12 @@ t.test("#ParseBarbatosSingle", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
t.test("Should return the score as a payload", (t) => {
|
||||
const res = ParseBarbatosSingle(barbScore as unknown as Record<string, unknown>, logger);
|
||||
const res = ParseBarbatosSingle(MockBarbatosScore as unknown as Record<string, unknown>, logger);
|
||||
|
||||
t.hasStrict(res, {
|
||||
game: "sdvx",
|
||||
context: {},
|
||||
iterable: [barbScore],
|
||||
iterable: [MockBarbatosScore],
|
||||
});
|
||||
|
||||
t.end();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { GetKTDataJSON } from "test-utils/test-data";
|
||||
import { FervidexStaticBase, GetKTDataJSON } from "test-utils/test-data";
|
||||
import { ParseFervidexStatic } from "./parser";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
@@ -9,10 +9,8 @@ const logger = CreateLogCtx(__filename);
|
||||
t.test("#ParseFervidexStatic", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
const ferStatic = GetKTDataJSON("./fervidex-static/base.json");
|
||||
|
||||
t.test("Should parse static data from body", (t) => {
|
||||
const res = ParseFervidexStatic(ferStatic, { model: "LDJ:J:B:A:2020092900" }, logger);
|
||||
const res = ParseFervidexStatic(FervidexStaticBase, { model: "LDJ:J:B:A:2020092900" }, logger);
|
||||
|
||||
t.strictSame(res.iterable, [
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ import CreateLogCtx from "lib/logger/logger";
|
||||
import t from "tap";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { GetKTDataJSON, Testing511Song, Testing511SPA } from "test-utils/test-data";
|
||||
import { InternalFailure } from "../../../framework/common/converter-failures";
|
||||
import { InternalFailure, InvalidScoreFailure } from "../../../framework/common/converter-failures";
|
||||
import {
|
||||
ConverterIRFervidex,
|
||||
SplitFervidexChartRef,
|
||||
@@ -81,7 +81,41 @@ t.test("#TachifyRandom", (t) => {
|
||||
t.end();
|
||||
});
|
||||
|
||||
const baseFervidexScore: FervidexScore = GetKTDataJSON("./fervidex/base.json");
|
||||
const baseFervidexScore: FervidexScore = {
|
||||
"bad": 0,
|
||||
"chart": "spa",
|
||||
"clear_type": 1,
|
||||
"combo_break": 6,
|
||||
"custom": false,
|
||||
"chart_sha256": "asdfasdf",
|
||||
"entry_id": 1000,
|
||||
"ex_score": 68,
|
||||
"fast": 0,
|
||||
"gauge": [
|
||||
100,
|
||||
50
|
||||
],
|
||||
"ghost": [
|
||||
0,
|
||||
2
|
||||
],
|
||||
"good": 0,
|
||||
"great": 0,
|
||||
"max_combo": 34,
|
||||
"option": {
|
||||
"gauge": "HARD",
|
||||
"range": "SUDDEN_PLUS",
|
||||
"style": "RANDOM"
|
||||
},
|
||||
"pacemaker": {
|
||||
"name": "",
|
||||
"score": 363,
|
||||
"type": "PACEMAKER_A"
|
||||
},
|
||||
"pgreat": 34,
|
||||
"poor": 6,
|
||||
"slow": 0
|
||||
}
|
||||
|
||||
const baseDryScore = {
|
||||
game: "iidx",
|
||||
@@ -208,7 +242,7 @@ t.test("#ConverterIRFervidex", (t) => {
|
||||
"ir/fervidex",
|
||||
logger
|
||||
),
|
||||
{ message: /could not find chart/giu }
|
||||
/could not find chart/giu
|
||||
);
|
||||
|
||||
t.end();
|
||||
@@ -224,7 +258,7 @@ t.test("#ConverterIRFervidex", (t) => {
|
||||
"ir/fervidex",
|
||||
logger
|
||||
),
|
||||
{ message: /Song 1 \(iidx\) has no parent song/giu }
|
||||
/Song 1 \(iidx\) has no parent song/giu
|
||||
);
|
||||
|
||||
t.end();
|
||||
@@ -239,7 +273,7 @@ t.test("#ConverterIRFervidex", (t) => {
|
||||
"ir/fervidex",
|
||||
logger
|
||||
),
|
||||
{ message: /Invalid percent/giu }
|
||||
/Invalid percent/giu
|
||||
);
|
||||
|
||||
t.end();
|
||||
@@ -254,7 +288,7 @@ t.test("#ConverterIRFervidex", (t) => {
|
||||
"ir/fervidex",
|
||||
logger
|
||||
),
|
||||
{ message: /Invalid value of gauge 150/giu }
|
||||
/Invalid value of gauge 150./giu
|
||||
);
|
||||
|
||||
t.end();
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
KTDataNotFoundFailure,
|
||||
} from "../../../framework/common/converter-failures";
|
||||
import { GenericGetGradeAndPercent } from "../../../framework/common/score-utils";
|
||||
import { IsNullishOrEmptyStr } from "utils/misc";
|
||||
import { FindIIDXChartOnInGameIDVersion, FindIIDXChartWith2DXtraHash } from "utils/queries/charts";
|
||||
import { FindSongOnID } from "utils/queries/songs";
|
||||
import type { DryScore } from "../../../framework/common/types";
|
||||
@@ -147,8 +148,8 @@ export const ConverterIRFervidex: ConverterFunction<FervidexScore, FervidexConte
|
||||
|
||||
let chart;
|
||||
|
||||
if (data.custom) {
|
||||
if (!data.chart_sha256) {
|
||||
if (data.custom === true) {
|
||||
if (IsNullishOrEmptyStr(data.chart_sha256)) {
|
||||
throw new InvalidScoreFailure("Score has no chart_sha256 but is a custom?");
|
||||
}
|
||||
|
||||
@@ -182,7 +183,9 @@ export const ConverterIRFervidex: ConverterFunction<FervidexScore, FervidexConte
|
||||
|
||||
const gauge = gaugeHistory[gaugeHistory.length - 1];
|
||||
|
||||
if (gauge && gauge > 100) {
|
||||
// If gauge exists and is greater than 100
|
||||
// must be invalid
|
||||
if ((gauge ?? 0) > 100) {
|
||||
throw new InvalidScoreFailure(`Invalid value of gauge ${gauge}.`);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,12 +44,12 @@ t.test("#SearchUsersRegExp", (t) => {
|
||||
const res = await SearchUsersRegExp("zkldi");
|
||||
|
||||
t.equal(res.length, 1);
|
||||
t.equal(res[0].usernameLowercase, "test_zkldi");
|
||||
t.equal(res[0]?.usernameLowercase, "test_zkldi");
|
||||
|
||||
const res2 = await SearchUsersRegExp("zk");
|
||||
|
||||
t.equal(res2.length, 1);
|
||||
t.equal(res2[0].usernameLowercase, "test_zkldi");
|
||||
t.equal(res2[0]?.usernameLowercase, "test_zkldi");
|
||||
|
||||
const res3 = await SearchUsersRegExp("zkzdi");
|
||||
|
||||
|
||||
+4
-2
@@ -38,7 +38,9 @@ async function RunOnInit() {
|
||||
await db["folder-chart-lookup"].findOne().then((r) => {
|
||||
// If there are no folder chart lookups, initialise them.
|
||||
if (!r) {
|
||||
InitaliseFolderChartLookup();
|
||||
InitaliseFolderChartLookup().catch((err: unknown) => {
|
||||
logger.error(`Failed to init folder-chart-lookup on first boot?`, { err });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -55,7 +57,7 @@ async function RunOnInit() {
|
||||
}
|
||||
}
|
||||
|
||||
RunOnInit();
|
||||
void RunOnInit();
|
||||
|
||||
let instance: http.Server | https.Server;
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { TachiConfig } from "lib/setup/config";
|
||||
import { ALL_PERMISSIONS, UserAuthLevels } from "tachi-common";
|
||||
import { SplitAuthorizationHeader } from "utils/misc";
|
||||
import { IsNullishOrEmptyStr, SplitAuthorizationHeader } from "utils/misc";
|
||||
import type { RequestHandler } from "express";
|
||||
import type { Session, SessionData } from "express-session";
|
||||
import type { APIPermissions, APITokenDocument } from "tachi-common";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
@@ -35,10 +36,15 @@ export const SetRequestPermissions: RequestHandler = CreateSetRequestPermissions
|
||||
*/
|
||||
function CreateSetRequestPermissions(errorKeyName: string): RequestHandler {
|
||||
return async (req, res, next) => {
|
||||
if (req.session.tachi?.user.id) {
|
||||
// Types here are wrong. Sometimes req.session is not set.
|
||||
// As such, we force an assertion.
|
||||
|
||||
const maybeSession = req.session as (Partial<SessionData> & Session) | undefined;
|
||||
|
||||
if (maybeSession?.tachi?.user.id !== undefined) {
|
||||
req[SYMBOL_TACHI_API_AUTH] = {
|
||||
userID: req.session.tachi.user.id,
|
||||
identifier: `Session-Key ${req.session.tachi.user.id}`,
|
||||
userID: maybeSession.tachi.user.id,
|
||||
identifier: `Session-Key ${maybeSession.tachi.user.id}`,
|
||||
token: null,
|
||||
permissions: ALL_PERMISSIONS,
|
||||
fromAPIClient: null,
|
||||
@@ -50,7 +56,7 @@ function CreateSetRequestPermissions(errorKeyName: string): RequestHandler {
|
||||
const header = req.header("Authorization");
|
||||
|
||||
// if no auth was attempted, default to the guest token.
|
||||
if (!header) {
|
||||
if (IsNullishOrEmptyStr(header)) {
|
||||
req[SYMBOL_TACHI_API_AUTH] = GuestToken;
|
||||
next();
|
||||
return;
|
||||
@@ -124,7 +130,7 @@ export const RequirePermissions =
|
||||
});
|
||||
}
|
||||
|
||||
if (!req[SYMBOL_TACHI_API_AUTH].userID) {
|
||||
if (req[SYMBOL_TACHI_API_AUTH].userID === null) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
description: `You are not authorised to perform this action.`,
|
||||
@@ -134,7 +140,7 @@ export const RequirePermissions =
|
||||
const missingPerms = [];
|
||||
|
||||
for (const perm of perms) {
|
||||
if (!req[SYMBOL_TACHI_API_AUTH]!.permissions[perm]) {
|
||||
if (req[SYMBOL_TACHI_API_AUTH].permissions[perm] !== true) {
|
||||
missingPerms.push(perm);
|
||||
}
|
||||
}
|
||||
@@ -185,7 +191,7 @@ export const RequireNotGuest: RequestHandler = CreateRequireNotGuest("descriptio
|
||||
export const FervidexStyleRequireNotGuest: RequestHandler = CreateRequireNotGuest("error");
|
||||
|
||||
export const RejectIfBanned: RequestHandler = async (req, res, next) => {
|
||||
if (req[SYMBOL_TACHI_API_AUTH].userID) {
|
||||
if (req[SYMBOL_TACHI_API_AUTH].userID !== null) {
|
||||
const isBanned = await db.users.findOne({
|
||||
id: req[SYMBOL_TACHI_API_AUTH].userID!,
|
||||
authLevel: UserAuthLevels.BANNED,
|
||||
|
||||
@@ -8,7 +8,8 @@ import type { integer } from "tachi-common";
|
||||
|
||||
const defaultLogger = CreateLogCtx(__filename);
|
||||
|
||||
export const DefaultMulterUpload = multer({ limits: { fileSize: 1024 * 1024 * 16 } }); // 16MB
|
||||
// 16MB
|
||||
export const DefaultMulterUpload = multer({ limits: { fileSize: 1024 * 1024 * 16 } });
|
||||
|
||||
export const CreateMulterSingleUploadMiddleware = (
|
||||
fieldName: string,
|
||||
@@ -44,6 +45,10 @@ export const CreateMulterSingleUploadMiddleware = (
|
||||
});
|
||||
}
|
||||
|
||||
// CRITICALLY IMPORTANT LINE OF CODE
|
||||
// THINGS DEALING WITH FILE UPLOADS **DO NOT** MOUNT SAFE-BODY OTHERWISE.
|
||||
req.safeBody = req.body as Record<string, unknown>;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,16 +17,9 @@ const ResJsonInteceptor = (res: Response, json: Response["json"]) => (content: u
|
||||
};
|
||||
|
||||
export const RequestLoggerMiddleware: RequestHandler = (req, res, next) => {
|
||||
// I'm not really a fan of this style of omission - but it works.
|
||||
|
||||
// we **KNOW** for certain that there are only two endpoints where a password is
|
||||
// sent to us - /register and /auth.
|
||||
// and both of those use `password` as a key.
|
||||
// still, i don't like it.
|
||||
|
||||
const safeBody: Record<string, unknown> = {};
|
||||
|
||||
for (const [k, v] of Object.entries(req.body)) {
|
||||
for (const [k, v] of Object.entries(req.safeBody)) {
|
||||
// Keys that start with ! are private information,
|
||||
// and should not ever be logged.
|
||||
if (k.startsWith("!")) {
|
||||
|
||||
@@ -11,17 +11,18 @@ import prValidate from "server/middleware/prudence-validate";
|
||||
import { UserAuthLevels } from "tachi-common";
|
||||
import { RecalcAllScores, UpdateAllPBs } from "utils/calculations/recalc-scores";
|
||||
import { RecalcSessions } from "utils/calculations/recalc-sessions";
|
||||
import { IsValidPlaytype } from "utils/misc";
|
||||
import DestroyUserGamePlaytypeData from "utils/reset-state/destroy-ugpt";
|
||||
import { GetUserWithID } from "utils/user";
|
||||
import type { RequestHandler } from "express";
|
||||
import type { Game } from "tachi-common";
|
||||
import type { Game, integer, Playtype } from "tachi-common";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
|
||||
const RequireAdminLevel: RequestHandler = async (req, res, next) => {
|
||||
if (!req[SYMBOL_TACHI_API_AUTH].userID) {
|
||||
if (req[SYMBOL_TACHI_API_AUTH].userID === null) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
description: `You are not authenticated.`,
|
||||
@@ -75,20 +76,26 @@ router.post(
|
||||
noReset: p.optional("boolean"),
|
||||
}),
|
||||
(req, res) => {
|
||||
const body = req.safeBody as {
|
||||
logLevel: "crit" | "debug" | "error" | "info" | "severe" | "verbose" | "warn";
|
||||
duration?: integer;
|
||||
noReset?: boolean;
|
||||
};
|
||||
|
||||
const logLevel = GetLogLevel();
|
||||
|
||||
ChangeRootLogLevel(req.body.logLevel);
|
||||
ChangeRootLogLevel(body.logLevel);
|
||||
|
||||
const duration = req.body.duration ?? 60;
|
||||
const duration = body.duration ?? 60;
|
||||
|
||||
if (currentLogLevelTimer) {
|
||||
logger.verbose(`Removing last timer to reset log level to ${LOG_LEVEL}.`);
|
||||
clearTimeout(currentLogLevelTimer);
|
||||
}
|
||||
|
||||
logger.info(`Log level has been changed to ${req.body.level}.`);
|
||||
logger.info(`Log level has been changed to ${body.logLevel}.`);
|
||||
|
||||
if (!req.body.noReset) {
|
||||
if (body.noReset !== true) {
|
||||
logger.info(`This will reset to "${LOG_LEVEL}" level in ${duration} minutes.`);
|
||||
|
||||
currentLogLevelTimer = setTimeout(() => {
|
||||
@@ -100,7 +107,7 @@ router.post(
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Changed log level from ${logLevel} to ${req.body.logLevel}.`,
|
||||
description: `Changed log level from ${logLevel} to ${body.logLevel}.`,
|
||||
body: {},
|
||||
});
|
||||
}
|
||||
@@ -121,7 +128,12 @@ router.post(
|
||||
filter: "*object",
|
||||
}),
|
||||
async (req, res) => {
|
||||
await UpdateAllPBs(req.body.userIDs, req.body.filter);
|
||||
const body = req.safeBody as {
|
||||
userIDs?: Array<integer>;
|
||||
filter?: object;
|
||||
};
|
||||
|
||||
await UpdateAllPBs(body.userIDs, body.filter);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
@@ -139,7 +151,9 @@ router.post(
|
||||
* @name POST /api/v1/admin/delete-score
|
||||
*/
|
||||
router.post("/delete-score", prValidate({ scoreID: "string" }), async (req, res) => {
|
||||
const score = await db.scores.findOne({ scoreID: req.body.scoreID });
|
||||
const body = req.safeBody as { scoreID: string };
|
||||
|
||||
const score = await db.scores.findOne({ scoreID: body.scoreID });
|
||||
|
||||
if (!score) {
|
||||
return res.status(404).json({
|
||||
@@ -171,14 +185,29 @@ router.post(
|
||||
prValidate({
|
||||
userID: p.isInteger,
|
||||
game: p.isIn(TachiConfig.GAMES),
|
||||
playtype: "string", // lazy
|
||||
playtype: (self, parent) => {
|
||||
if (typeof self !== "string") {
|
||||
return "Expected a string for a playtype.";
|
||||
}
|
||||
|
||||
if (!IsValidPlaytype(parent.game as Game, self)) {
|
||||
return `Invalid playtype of ${self} for game ${parent.game as Game}.`;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
async (req, res) => {
|
||||
const { userID, game, playtype } = req.body;
|
||||
const { userID, game, playtype } = req.safeBody as {
|
||||
userID: integer;
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
};
|
||||
|
||||
const ugpt = await db["game-stats"].findOne({
|
||||
userID,
|
||||
game,
|
||||
|
||||
playtype,
|
||||
});
|
||||
|
||||
@@ -211,8 +240,12 @@ router.post(
|
||||
"/destroy-chart",
|
||||
prValidate({ chartID: "string", game: p.isIn(TachiConfig.GAMES) }),
|
||||
async (req, res) => {
|
||||
const game: Game = req.body.game;
|
||||
const chartID: string = req.body.chartID;
|
||||
const body = req.safeBody as {
|
||||
game: Game;
|
||||
chartID: string;
|
||||
};
|
||||
|
||||
const { game, chartID } = body;
|
||||
|
||||
const scores = await db.scores.find({
|
||||
chartID,
|
||||
@@ -242,7 +275,7 @@ router.post(
|
||||
* @name POST /api/v1/admin/recalc
|
||||
*/
|
||||
router.post("/recalc", async (req, res) => {
|
||||
const filter = req.body ?? {};
|
||||
const filter = req.safeBody;
|
||||
|
||||
await RecalcAllScores(filter);
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
GetUserCaseInsensitive,
|
||||
GetUserPrivateInfo,
|
||||
GetUserWithID,
|
||||
GetUserWithIDGuaranteed,
|
||||
} from "utils/user";
|
||||
import type { integer } from "tachi-common";
|
||||
|
||||
@@ -60,17 +61,23 @@ router.post(
|
||||
"verbose"
|
||||
),
|
||||
async (req, res) => {
|
||||
if (req.session.tachi?.user.id) {
|
||||
if (req.session.tachi?.user.id !== undefined) {
|
||||
// Dual logins should destroy the users session and recreate it.
|
||||
req.session.tachi = undefined;
|
||||
}
|
||||
|
||||
logger.verbose(`Received login request with username ${req.body.username} (${req.ip})`);
|
||||
const body = req.safeBody as {
|
||||
username: string;
|
||||
"!password": string;
|
||||
captcha: string;
|
||||
};
|
||||
|
||||
logger.verbose(`Received login request with username ${body.username} (${req.ip})`);
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (Environment.nodeEnv === "production" || Environment.nodeEnv === "staging") {
|
||||
logger.verbose("Validating captcha...");
|
||||
const validCaptcha = await ValidateCaptcha(req.body.captcha, req.socket.remoteAddress);
|
||||
const validCaptcha = await ValidateCaptcha(body.captcha, req.socket.remoteAddress);
|
||||
|
||||
if (!validCaptcha) {
|
||||
logger.verbose("Captcha failed.");
|
||||
@@ -85,10 +92,10 @@ router.post(
|
||||
logger.warn("Skipped captcha check because not in production.");
|
||||
}
|
||||
|
||||
const requestedUser = await GetUserCaseInsensitive(req.body.username);
|
||||
const requestedUser = await GetUserCaseInsensitive(body.username);
|
||||
|
||||
if (!requestedUser) {
|
||||
logger.verbose(`Invalid username for login ${req.body.username}.`);
|
||||
logger.verbose(`Invalid username for login ${body.username}.`);
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
description: `This user does not exist.`,
|
||||
@@ -111,7 +118,7 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
const passwordMatch = await PasswordCompare(req.body["!password"], privateInfo.password);
|
||||
const passwordMatch = await PasswordCompare(body["!password"], privateInfo.password);
|
||||
|
||||
if (!passwordMatch) {
|
||||
logger.verbose("Invalid password provided.");
|
||||
@@ -178,12 +185,27 @@ router.post(
|
||||
"verbose"
|
||||
),
|
||||
async (req, res) => {
|
||||
logger.verbose(`received register request with username ${req.body.username} (${req.ip})`);
|
||||
const body = req.safeBody as {
|
||||
username: string;
|
||||
"!password": string;
|
||||
email: string;
|
||||
inviteCode?: string;
|
||||
captcha: string;
|
||||
};
|
||||
|
||||
if (body.inviteCode === undefined && ServerConfig.INVITE_CODE_CONFIG) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `No invite code given, yet the server uses invites.`,
|
||||
});
|
||||
}
|
||||
|
||||
logger.verbose(`received register request with username ${body.username} (${req.ip})`);
|
||||
|
||||
/* istanbul ignore next */
|
||||
if (Environment.nodeEnv === "production" || Environment.nodeEnv === "staging") {
|
||||
logger.verbose("Validating captcha...");
|
||||
const validCaptcha = await ValidateCaptcha(req.body.captcha, req.socket.remoteAddress);
|
||||
const validCaptcha = await ValidateCaptcha(body.captcha, req.socket.remoteAddress);
|
||||
|
||||
if (!validCaptcha) {
|
||||
logger.verbose("Captcha failed.");
|
||||
@@ -198,17 +220,17 @@ router.post(
|
||||
logger.warn("Skipped captcha check because not in production.");
|
||||
}
|
||||
|
||||
const existingUser = await GetUserCaseInsensitive(req.body.username);
|
||||
const existingUser = await GetUserCaseInsensitive(body.username);
|
||||
|
||||
if (existingUser) {
|
||||
logger.verbose(`Invalid username ${req.body.username}, already in use.`);
|
||||
logger.verbose(`Invalid username ${body.username}, already in use.`);
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
description: "This username is already in use.",
|
||||
});
|
||||
}
|
||||
|
||||
const existingEmail = await CheckIfEmailInUse(req.body.email);
|
||||
const existingEmail = await CheckIfEmailInUse(body.email);
|
||||
|
||||
if (existingEmail) {
|
||||
logger.info(`User attempted to sign up with email that was already in use.`);
|
||||
@@ -226,7 +248,7 @@ router.post(
|
||||
if (ServerConfig.INVITE_CODE_CONFIG) {
|
||||
const inviteCodeDoc = await db.invites.findOneAndUpdate(
|
||||
{
|
||||
code: req.body.inviteCode,
|
||||
code: body.inviteCode,
|
||||
consumed: false,
|
||||
},
|
||||
{
|
||||
@@ -239,7 +261,7 @@ router.post(
|
||||
);
|
||||
|
||||
if (!inviteCodeDoc) {
|
||||
logger.info(`Invalid invite code given: ${req.body.inviteCode}.`);
|
||||
logger.info(`Invalid invite code given: ${body.inviteCode}.`);
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
description: `This invite code is not valid.`,
|
||||
@@ -252,52 +274,48 @@ router.post(
|
||||
// if we get to this point, We're good to create the user.
|
||||
|
||||
const { newUser, newSettings } = await AddNewUser(
|
||||
req.body.username,
|
||||
req.body["!password"],
|
||||
req.body.email,
|
||||
body.username,
|
||||
body["!password"],
|
||||
body.email,
|
||||
userID
|
||||
);
|
||||
|
||||
if (!newUser) {
|
||||
throw new Error("AddNewUser failed to create a user.");
|
||||
}
|
||||
|
||||
hasInsertedUserID = newUser.id;
|
||||
|
||||
// re-fetch the user like this so we guaranteeably omit the private fields.
|
||||
const user = await GetUserWithID(newUser.id);
|
||||
const user = await GetUserWithIDGuaranteed(newUser.id);
|
||||
|
||||
MountAuthCookie(req, user!, newSettings);
|
||||
MountAuthCookie(req, user, newSettings);
|
||||
|
||||
const resetEmailCode = Random20Hex();
|
||||
|
||||
await db["verify-email-codes"].insert({
|
||||
code: resetEmailCode,
|
||||
userID,
|
||||
email: req.body.email,
|
||||
email: body.email,
|
||||
});
|
||||
|
||||
const { text, html } = EmailFormatVerifyEmail(user!.username, resetEmailCode);
|
||||
const { text, html } = EmailFormatVerifyEmail(user.username, resetEmailCode);
|
||||
|
||||
SendEmail(req.body.email, "Email Verification", html, text);
|
||||
void SendEmail(body.email, "Email Verification", html, text);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Successfully created account ${req.body.username}!`,
|
||||
description: `Successfully created account ${body.username}!`,
|
||||
body: user,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error(`Bailed on user creation ${req.body.username}.`, { err });
|
||||
logger.error(`Bailed on user creation ${body.username}.`, { err });
|
||||
|
||||
if (ServerConfig.INVITE_CODE_CONFIG) {
|
||||
await ReinstateInvite(req.body.inviteCode);
|
||||
if (ServerConfig.INVITE_CODE_CONFIG && body.inviteCode !== undefined) {
|
||||
await ReinstateInvite(body.inviteCode);
|
||||
}
|
||||
|
||||
if (hasInsertedUserID !== null) {
|
||||
logger.warn(
|
||||
`Removing user ${req.body.username} (#${hasInsertedUserID}), as their document was created, but creation still failed.`
|
||||
`Removing user ${body.username} (#${hasInsertedUserID}), as their document was created, but creation still failed.`
|
||||
);
|
||||
await db.users.remove({ username: req.body.username });
|
||||
await db.users.remove({ username: body.username });
|
||||
await db["user-settings"].remove({ userID: hasInsertedUserID });
|
||||
await db["user-private-information"].remove({ userID: hasInsertedUserID });
|
||||
}
|
||||
@@ -326,8 +344,12 @@ router.post(
|
||||
code: "string",
|
||||
}),
|
||||
async (req, res) => {
|
||||
const body = req.safeBody as {
|
||||
code: string;
|
||||
};
|
||||
|
||||
const code = await db["verify-email-codes"].findOne({
|
||||
code: req.body.code,
|
||||
code: body.code,
|
||||
});
|
||||
|
||||
if (!code) {
|
||||
@@ -338,7 +360,7 @@ router.post(
|
||||
}
|
||||
|
||||
await db["verify-email-codes"].remove({
|
||||
code: req.body.code,
|
||||
code: body.code,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -387,7 +409,7 @@ router.post("/resend-verify-email", HyperAggressiveRateLimitMiddleware, async (r
|
||||
|
||||
const { text, html } = EmailFormatVerifyEmail(user.username, verifyInfo.code);
|
||||
|
||||
SendEmail(verifyInfo.email, "Email Verification", html, text);
|
||||
void SendEmail(verifyInfo.email, "Email Verification", html, text);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -395,7 +417,7 @@ router.post("/resend-verify-email", HyperAggressiveRateLimitMiddleware, async (r
|
||||
* @name POST /api/v1/auth/logout
|
||||
*/
|
||||
router.post("/logout", (req, res) => {
|
||||
if (!req.session.tachi?.user.id) {
|
||||
if (req.session.tachi?.user.id === undefined) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
description: `You are not logged in.`,
|
||||
@@ -431,7 +453,11 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug(`received password reset request for ${req.body.email}.`);
|
||||
const body = req.safeBody as {
|
||||
email: string;
|
||||
};
|
||||
|
||||
logger.debug(`received password reset request for ${body.email}.`);
|
||||
|
||||
// For timing attack and infosec reasons, we can't do anything but **immediately** return here.
|
||||
res.status(202).json({
|
||||
@@ -441,7 +467,7 @@ router.post(
|
||||
});
|
||||
|
||||
const userPrivateInfo = await db["user-private-information"].findOne({
|
||||
email: req.body.email,
|
||||
email: body.email,
|
||||
});
|
||||
|
||||
if (userPrivateInfo) {
|
||||
@@ -466,10 +492,10 @@ router.post(
|
||||
|
||||
const { html, text } = EmailFormatResetPassword(user.username, code, req.ip);
|
||||
|
||||
SendEmail(userPrivateInfo.email, "Reset Password", html, text);
|
||||
void SendEmail(userPrivateInfo.email, "Reset Password", html, text);
|
||||
} else {
|
||||
logger.info(
|
||||
`Silently rejected password reset request for ${req.body.email}, as no user has this email.`
|
||||
`Silently rejected password reset request for ${body.email}, as no user has this email.`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -492,8 +518,13 @@ router.post(
|
||||
"!password": ValidatePassword,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const body = req.safeBody as {
|
||||
code: string;
|
||||
"!password": string;
|
||||
};
|
||||
|
||||
const code = await db["password-reset-codes"].findOneAndDelete({
|
||||
code: req.body.code,
|
||||
code: body.code,
|
||||
});
|
||||
|
||||
if (!code) {
|
||||
@@ -503,7 +534,7 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
const encryptedPassword = await HashPassword(req.body["!password"]);
|
||||
const encryptedPassword = await HashPassword(body["!password"]);
|
||||
|
||||
await db["user-private-information"].update(
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ t.test("#GetClientFromID", (t) => {
|
||||
params: {
|
||||
clientID: "OAUTH2_CLIENT_ID",
|
||||
},
|
||||
[SYMBOL_TACHI_DATA]: {}
|
||||
});
|
||||
|
||||
t.strictSame(
|
||||
@@ -27,7 +28,7 @@ t.test("#GetClientFromID", (t) => {
|
||||
apiKeyTemplate: null,
|
||||
apiKeyFilename: null,
|
||||
},
|
||||
"Should assign clientDoc with secret ommitted."
|
||||
"Should assign clientDoc with secret omitted."
|
||||
);
|
||||
|
||||
t.end();
|
||||
@@ -38,6 +39,7 @@ t.test("#GetClientFromID", (t) => {
|
||||
params: {
|
||||
clientID: "NONSENSE",
|
||||
},
|
||||
[SYMBOL_TACHI_DATA]: {}
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 404);
|
||||
@@ -58,11 +60,12 @@ t.test("#RequireOwnershipOfClient", (t) => {
|
||||
|
||||
t.test("Should return 401 if the user has no authentication.", async (t) => {
|
||||
const { res } = await expMiddlewareMock(RequireOwnershipOfClient, {
|
||||
body: {
|
||||
safeBody: {
|
||||
__terribleHackOauth2ClientDoc: {
|
||||
author: 1,
|
||||
},
|
||||
},
|
||||
[SYMBOL_TACHI_DATA]: {},
|
||||
session: {},
|
||||
});
|
||||
|
||||
@@ -73,7 +76,7 @@ t.test("#RequireOwnershipOfClient", (t) => {
|
||||
|
||||
t.test("Should return 403 if the user does not own this client.", async (t) => {
|
||||
const { res } = await expMiddlewareMock(RequireOwnershipOfClient, {
|
||||
body: {
|
||||
safeBody: {
|
||||
__terribleHackOauth2ClientDoc: {
|
||||
author: 1,
|
||||
},
|
||||
@@ -85,6 +88,7 @@ t.test("#RequireOwnershipOfClient", (t) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
[SYMBOL_TACHI_DATA]: {},
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 403);
|
||||
@@ -94,7 +98,7 @@ t.test("#RequireOwnershipOfClient", (t) => {
|
||||
|
||||
t.test("Should continue if this is their session.", async (t) => {
|
||||
const { res } = await expMiddlewareMock(RequireOwnershipOfClient, {
|
||||
body: {
|
||||
safeBody: {
|
||||
__terribleHackOauth2ClientDoc: {
|
||||
author: 1,
|
||||
},
|
||||
@@ -106,6 +110,7 @@ t.test("#RequireOwnershipOfClient", (t) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
[SYMBOL_TACHI_DATA]: {},
|
||||
});
|
||||
|
||||
t.equal(res.statusCode, 200);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import db from "external/mongo/db";
|
||||
import { SYMBOL_TACHI_DATA } from "lib/constants/tachi";
|
||||
import { Environment } from "lib/setup/config";
|
||||
import { AssignToReqTachiData, GetTachiData } from "utils/req-tachi-data";
|
||||
import type { RequestHandler } from "express";
|
||||
@@ -37,10 +36,13 @@ export const RequireOwnershipOfClient: RequestHandler = (req, res, next) => {
|
||||
// request. To hack around this for testing, we perform this hack.
|
||||
// There's an open issue for this here: https://github.com/i-like-robots/express-request-mock/issues/19
|
||||
/* istanbul ignore next */
|
||||
if (Environment.nodeEnv === "test" && req.body.__terribleHackOauth2ClientDoc) {
|
||||
if (
|
||||
Environment.nodeEnv === "test" &&
|
||||
(req.safeBody.__terribleHackOauth2ClientDoc as TachiAPIClientDocument | undefined)
|
||||
) {
|
||||
// obviously a glaring hack and security flaw - this only applies
|
||||
// in testing.
|
||||
client = req.body.__terribleHackOauth2ClientDoc;
|
||||
client = req.safeBody.__terribleHackOauth2ClientDoc as TachiAPIClientDocument;
|
||||
} else {
|
||||
client = GetTachiData(req, "apiClientDoc");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { GetClientFromID, RequireOwnershipOfClient } from "./middleware";
|
||||
import { Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import { SYMBOL_TACHI_DATA } from "lib/constants/tachi";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ServerConfig } from "lib/setup/config";
|
||||
import p from "prudence";
|
||||
@@ -90,6 +89,15 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
const body = req.safeBody as {
|
||||
name: string;
|
||||
redirectUri: string | null;
|
||||
webhookUri: string | null;
|
||||
apiKeyTemplate: string | null;
|
||||
apiKeyFilename: string | null;
|
||||
permissions: Array<APIPermissions>;
|
||||
};
|
||||
|
||||
const existingClients = await db["api-clients"].find({
|
||||
author: req.session.tachi.user.id,
|
||||
});
|
||||
@@ -105,7 +113,7 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
const permissions = DedupeArr<APIPermissions>(req.body.permissions);
|
||||
const permissions = DedupeArr<APIPermissions>(body.permissions);
|
||||
|
||||
if (permissions.length === 0) {
|
||||
return res.status(400).json({
|
||||
@@ -114,14 +122,14 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.redirectUri !== null && !IsValidURL(req.body.redirectUri)) {
|
||||
if (body.redirectUri !== null && !IsValidURL(body.redirectUri)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid Redirect URL.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.webhookUri !== null && !IsValidURL(req.body.webhookUri)) {
|
||||
if (body.webhookUri !== null && !IsValidURL(body.webhookUri)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid Webhook URL.`,
|
||||
@@ -135,19 +143,19 @@ router.post(
|
||||
clientID,
|
||||
clientSecret,
|
||||
requestedPermissions: permissions,
|
||||
name: req.body.name,
|
||||
name: body.name,
|
||||
author: req.session.tachi.user.id,
|
||||
redirectUri: req.body.redirectUri,
|
||||
webhookUri: req.body.webhookUri ?? null,
|
||||
apiKeyFilename: req.body.apiKeyFilename ?? null,
|
||||
apiKeyTemplate: req.body.apiKeyTemplate ?? null,
|
||||
redirectUri: body.redirectUri,
|
||||
webhookUri: body.webhookUri ?? null,
|
||||
apiKeyFilename: body.apiKeyFilename ?? null,
|
||||
apiKeyTemplate: body.apiKeyTemplate ?? null,
|
||||
};
|
||||
|
||||
await db["api-clients"].insert(clientDoc);
|
||||
|
||||
logger.info(
|
||||
`User ${FormatUserDoc(req.session.tachi.user)} created a new API Client ${
|
||||
req.body.name
|
||||
body.name
|
||||
} (${clientID}).`
|
||||
);
|
||||
|
||||
@@ -232,11 +240,20 @@ router.patch(
|
||||
}),
|
||||
}),
|
||||
async (req, res) => {
|
||||
const body = req.safeBody as {
|
||||
name?: string;
|
||||
redirectUri?: string | null;
|
||||
webhookUri?: string | null;
|
||||
apiKeyTemplate?: string | null;
|
||||
apiKeyFilename?: string | null;
|
||||
permissions?: Array<APIPermissions>;
|
||||
};
|
||||
|
||||
const client = GetTachiData(req, "apiClientDoc");
|
||||
|
||||
DeleteUndefinedProps(req.body);
|
||||
DeleteUndefinedProps(req.safeBody);
|
||||
|
||||
if (Object.keys(req.body).length === 0) {
|
||||
if (Object.keys(req.safeBody).length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `No changes to make.`,
|
||||
@@ -248,12 +265,12 @@ router.patch(
|
||||
clientID: client.clientID,
|
||||
},
|
||||
{
|
||||
$set: req.body,
|
||||
$set: req.safeBody,
|
||||
}
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`API Client ${client.name} (${client.clientID}) has been renamed to ${req.body.name}.`
|
||||
`API Client ${client.name} (${client.clientID}) has been renamed to ${body.name}.`
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -17,7 +17,7 @@ const LoadLazySampleData = async () => {
|
||||
await db.goals.insert(IIDXSPMilestoneGoals);
|
||||
await db["goal-subs"].insert([
|
||||
...IIDXSPMilestoneGoalSubs,
|
||||
dm(IIDXSPMilestoneGoalSubs[0], {
|
||||
dm(IIDXSPMilestoneGoalSubs[0]!, {
|
||||
userID: 2,
|
||||
}),
|
||||
] as GoalSubscriptionDocument[]);
|
||||
|
||||
@@ -2,6 +2,7 @@ import dm from "deepmerge";
|
||||
import db from "external/mongo/db";
|
||||
import { GoalSubscriptionDocument } from "tachi-common";
|
||||
import t from "tap";
|
||||
import { mkFakeGoal, mkFakeGoalSub } from "test-utils/misc";
|
||||
import mockApi from "test-utils/mock-api";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { HC511UserGoal } from "test-utils/test-data";
|
||||
@@ -9,18 +10,19 @@ import { HC511UserGoal } from "test-utils/test-data";
|
||||
t.test("GET /api/v1/games/:game/:playtype/targets/recently-achieved", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
// mutate
|
||||
function m(partial: Partial<GoalSubscriptionDocument>): GoalSubscriptionDocument {
|
||||
return dm(HC511UserGoal, partial);
|
||||
}
|
||||
|
||||
t.test("Should return some recently achieved goals.", async (t) => {
|
||||
await db.goals.insert([
|
||||
mkFakeGoal({ goalID: "achieved" }),
|
||||
mkFakeGoal({ goalID: "achieved_more_recently" }),
|
||||
mkFakeGoal({ goalID: "achieved_instantly" }),
|
||||
])
|
||||
|
||||
await db["goal-subs"].insert([
|
||||
// not achieved
|
||||
HC511UserGoal,
|
||||
m({ goalID: "achieved", achieved: true, timeAchieved: 1000 }),
|
||||
m({ goalID: "achieved_more_recently", achieved: true, timeAchieved: 2000 }),
|
||||
m({
|
||||
mkFakeGoalSub({ goalID: "achieved", achieved: true, timeAchieved: 1000 }),
|
||||
mkFakeGoalSub({ goalID: "achieved_more_recently", achieved: true, timeAchieved: 2000 }),
|
||||
mkFakeGoalSub({
|
||||
goalID: "achieved_instantly",
|
||||
achieved: true,
|
||||
timeAchieved: 1000,
|
||||
@@ -48,25 +50,27 @@ t.test("GET /api/v1/games/:game/:playtype/targets/recently-achieved", (t) => {
|
||||
t.test("GET /api/v1/games/:game/:playtype/targets/recently-raised", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
|
||||
// mutate
|
||||
function m(partial: Partial<GoalSubscriptionDocument>): GoalSubscriptionDocument {
|
||||
return dm(HC511UserGoal, partial);
|
||||
}
|
||||
|
||||
t.test("Should return some recently interacted goals.", async (t) => {
|
||||
await db.goals.insert([
|
||||
mkFakeGoal({ goalID: "interacted" }),
|
||||
mkFakeGoal({ goalID: "interacted_more_recently" }),
|
||||
mkFakeGoal({ goalID: "achieved" }),
|
||||
mkFakeGoal({ goalID: "achieved_instantly" }),
|
||||
]);
|
||||
|
||||
await db["goal-subs"].insert([
|
||||
// not achieved
|
||||
HC511UserGoal,
|
||||
m({ goalID: "interacted", achieved: false, lastInteraction: 1000 }),
|
||||
mkFakeGoalSub({ goalID: "interacted", achieved: false, lastInteraction: 1000 }),
|
||||
// happened more recently
|
||||
m({ goalID: "interacted_more_recently", achieved: false, lastInteraction: 2000 }),
|
||||
mkFakeGoalSub({ goalID: "interacted_more_recently", achieved: false, lastInteraction: 2000 }),
|
||||
// shouldnt be included -- just recently-raised.
|
||||
m({
|
||||
mkFakeGoalSub({
|
||||
goalID: "achieved",
|
||||
achieved: true,
|
||||
lastInteraction: 1000,
|
||||
}),
|
||||
m({
|
||||
mkFakeGoalSub({
|
||||
goalID: "achieved_instantly",
|
||||
achieved: true,
|
||||
lastInteraction: 1000,
|
||||
|
||||
@@ -58,7 +58,7 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
const importType = req.body.importType as FileUploadImportTypes;
|
||||
const importType = req.safeBody.importType as FileUploadImportTypes;
|
||||
|
||||
const userIntent = !!req.header("X-User-Intent");
|
||||
|
||||
@@ -70,11 +70,11 @@ router.post(
|
||||
userID: req[SYMBOL_TACHI_API_AUTH].userID!,
|
||||
userIntent,
|
||||
importType,
|
||||
parserArguments: [req.file, req.body],
|
||||
parserArguments: [req.file, req.safeBody],
|
||||
};
|
||||
|
||||
// Fire the score import, but make no guarantees about its state.
|
||||
MakeScoreImport<FileUploadImportTypes>(job);
|
||||
void MakeScoreImport<FileUploadImportTypes>(job);
|
||||
|
||||
return res.status(202).json({
|
||||
success: true,
|
||||
@@ -92,7 +92,7 @@ router.post(
|
||||
req[SYMBOL_TACHI_API_AUTH].userID!,
|
||||
userIntent,
|
||||
importType,
|
||||
[req.file, req.body]
|
||||
[req.file, req.safeBody]
|
||||
);
|
||||
|
||||
return res.status(importResponse.statusCode).json(importResponse.body);
|
||||
@@ -114,7 +114,7 @@ router.post(
|
||||
{ allowExcessKeys: true }
|
||||
),
|
||||
async (req, res) => {
|
||||
const importType = req.body.importType as APIImportTypes;
|
||||
const importType = req.safeBody.importType as APIImportTypes;
|
||||
|
||||
const importID = Random20Hex();
|
||||
|
||||
|
||||
@@ -31,8 +31,16 @@ router.post(
|
||||
code: "string",
|
||||
}),
|
||||
async (req, res) => {
|
||||
const body = req.safeBody as {
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
grant_type: "authorization_code";
|
||||
redirect_uri: string;
|
||||
code: string;
|
||||
};
|
||||
|
||||
const client = await db["api-clients"].findOne({
|
||||
clientID: req.body.client_id,
|
||||
clientID: body.client_id,
|
||||
});
|
||||
|
||||
if (!client) {
|
||||
@@ -42,7 +50,7 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
if (client.clientSecret !== req.body.client_secret) {
|
||||
if (client.clientSecret !== body.client_secret) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
description: `Invalid secret.`,
|
||||
@@ -51,14 +59,14 @@ router.post(
|
||||
|
||||
// I honest to god have no idea what the point of this check is
|
||||
// but it's part of the oauth spec.
|
||||
if (client.redirectUri !== req.body.redirect_uri) {
|
||||
if (client.redirectUri !== body.redirect_uri) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `This redirect_uri does not match with your registered client redirect_uri ${client.redirectUri}.`,
|
||||
});
|
||||
}
|
||||
|
||||
const codeDoc = await db["oauth2-auth-codes"].findOne({ code: req.body.code });
|
||||
const codeDoc = await db["oauth2-auth-codes"].findOne({ code: body.code });
|
||||
|
||||
if (!codeDoc) {
|
||||
return res.status(404).json({
|
||||
@@ -68,7 +76,7 @@ router.post(
|
||||
}
|
||||
|
||||
// don't let people auth with the same code multiple times.
|
||||
await db["oauth2-auth-codes"].remove({ code: req.body.code });
|
||||
await db["oauth2-auth-codes"].remove({ code: body.code });
|
||||
|
||||
const apiDoc = {
|
||||
userID: codeDoc.userID,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { GetScoreFromParam, RequireOwnershipOfScoreOrAdmin } from "./middleware";
|
||||
import { Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import { SYMBOL_TACHI_DATA } from "lib/constants/tachi";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { DeleteScore } from "lib/score-mutation/delete-scores";
|
||||
import p from "prudence";
|
||||
@@ -26,7 +25,7 @@ router.use(GetScoreFromParam);
|
||||
router.get("/", async (req, res) => {
|
||||
const score = GetTachiData(req, "scoreDoc");
|
||||
|
||||
if (req.query.getRelated) {
|
||||
if (req.query.getRelated !== undefined) {
|
||||
const [user, chart, song] = await Promise.all([
|
||||
GetUserWithID(score.userID),
|
||||
db.charts[score.game].findOne({ chartID: score.chartID }),
|
||||
@@ -88,16 +87,21 @@ router.patch(
|
||||
highlight: "*boolean",
|
||||
}),
|
||||
async (req, res) => {
|
||||
const body = req.safeBody as {
|
||||
comment?: string | null;
|
||||
highlight?: boolean;
|
||||
};
|
||||
|
||||
const score = GetTachiData(req, "scoreDoc");
|
||||
|
||||
const modifyOption: ModifiableScoreProps = {};
|
||||
|
||||
if (req.body.comment !== undefined) {
|
||||
modifyOption.comment = req.body.comment;
|
||||
if (body.comment !== undefined) {
|
||||
modifyOption.comment = body.comment;
|
||||
}
|
||||
|
||||
if (req.body.highlight !== undefined) {
|
||||
modifyOption.highlight = req.body.highlight;
|
||||
if (body.highlight !== undefined) {
|
||||
modifyOption.highlight = body.highlight;
|
||||
}
|
||||
|
||||
if (Object.keys(modifyOption).length === 0) {
|
||||
@@ -145,11 +149,16 @@ router.patch(
|
||||
router.delete(
|
||||
"/",
|
||||
RequireOwnershipOfScoreOrAdmin,
|
||||
prValidate({ blacklist: "*boolean" }),
|
||||
RequirePermissions("delete_score"),
|
||||
async (req, res) => {
|
||||
const body = req.safeBody as {
|
||||
blacklist?: boolean;
|
||||
};
|
||||
|
||||
const score = GetTachiData(req, "scoreDoc");
|
||||
|
||||
await DeleteScore(score, !!req.body.blacklist);
|
||||
await DeleteScore(score, body.blacklist);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
|
||||
@@ -87,16 +87,16 @@ router.patch(
|
||||
|
||||
const updateExp: ModifiableSessionProps = {};
|
||||
|
||||
if (req.body.name) {
|
||||
updateExp.name = req.body.name as string;
|
||||
if (req.safeBody.name) {
|
||||
updateExp.name = req.safeBody.name as string;
|
||||
}
|
||||
|
||||
if (req.body.desc) {
|
||||
updateExp.desc = req.body.desc as string;
|
||||
if (req.safeBody.desc) {
|
||||
updateExp.desc = req.safeBody.desc as string;
|
||||
}
|
||||
|
||||
if (typeof req.body.highlight === "boolean") {
|
||||
updateExp.highlight = req.body.highlight as boolean;
|
||||
if (typeof req.safeBody.highlight === "boolean") {
|
||||
updateExp.highlight = req.safeBody.highlight;
|
||||
}
|
||||
|
||||
if (Object.keys(updateExp).length === 0) {
|
||||
|
||||
@@ -45,8 +45,8 @@ router.get("/", (req, res) => {
|
||||
router.post("/", (req, res) => {
|
||||
let echo;
|
||||
|
||||
if (req.body.echo && typeof req.body.echo === "string") {
|
||||
echo = req.body.echo;
|
||||
if (req.safeBody.echo && typeof req.safeBody.echo === "string") {
|
||||
echo = req.safeBody.echo;
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { RequireSelfRequestFromUser } from "../middleware";
|
||||
import { Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import { SYMBOL_TACHI_DATA } from "lib/constants/tachi";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import p from "prudence";
|
||||
import prValidate from "server/middleware/prudence-validate";
|
||||
@@ -55,7 +54,13 @@ router.post(
|
||||
clientID: "*string",
|
||||
}),
|
||||
async (req, res) => {
|
||||
if (req.body.clientID && req.body.permissions) {
|
||||
const body = req.safeBody as {
|
||||
permissions?: Array<APIPermissions>;
|
||||
identifier?: string;
|
||||
clientID?: string;
|
||||
};
|
||||
|
||||
if (body.clientID !== undefined && body.permissions) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Cannot use ClientID creation and permissions creation at the same time!`,
|
||||
@@ -69,10 +74,10 @@ router.post(
|
||||
let identifier: string;
|
||||
let fromAPIClient = null;
|
||||
|
||||
if (req.body.clientID) {
|
||||
if (body.clientID !== undefined) {
|
||||
const client = await db["api-clients"].findOne(
|
||||
{
|
||||
clientID: req.body.clientID,
|
||||
clientID: body.clientID,
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
@@ -108,9 +113,9 @@ router.post(
|
||||
logger.info(
|
||||
`Creating API Key for ${FormatUserDoc(user)} from ${client.name} specification.`
|
||||
);
|
||||
} else if (req.body.permissions) {
|
||||
permissions = req.body.permissions;
|
||||
identifier = req.body.identifier ?? "Custom Token";
|
||||
} else if (body.permissions) {
|
||||
permissions = body.permissions;
|
||||
identifier = body.identifier ?? "Custom Token";
|
||||
|
||||
logger.info(
|
||||
`Creating API Key for ${FormatUserDoc(user)} with ${permissions.join(", ")}.`
|
||||
@@ -144,39 +149,4 @@ router.post(
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Delete this token.
|
||||
*
|
||||
* @name DELETE /api/v1/users/:userID/api-token/:token
|
||||
*/
|
||||
router.delete("/:token", async (req, res) => {
|
||||
const user = GetTachiData(req, "requestedUser");
|
||||
|
||||
logger.info(
|
||||
`received request from ${FormatUserDoc(user)} to delete token ${req.params.token}.`
|
||||
);
|
||||
|
||||
const token = await db["api-tokens"].findOne({
|
||||
token: req.params.token,
|
||||
userID: user.id,
|
||||
});
|
||||
|
||||
if (!token) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
description: `This key does not exist.`,
|
||||
});
|
||||
}
|
||||
|
||||
await db["api-tokens"].remove({ token: req.params.token });
|
||||
|
||||
logger.info(`Deleted ${req.params.token}, which belonged to ${FormatUserDoc(user)}.`);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
description: `Removed Token.`,
|
||||
body: {},
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import deepmerge from "deepmerge";
|
||||
import db from "external/mongo/db";
|
||||
import { FolderDocument, ScoreDocument } from "tachi-common";
|
||||
import { ChartDocument, FolderDocument, ScoreDocument, SongDocument } from "tachi-common";
|
||||
import t from "tap";
|
||||
import mockApi from "test-utils/mock-api";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
@@ -65,8 +65,8 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/folders/:folderID/timeli
|
||||
await db.songs.iidx.remove({});
|
||||
await db.charts.iidx.remove({});
|
||||
await db["folder-chart-lookup"].remove({});
|
||||
await db.songs.iidx.insert(GetKTDataJSON("./tachi/tachi-songs-iidx.json"));
|
||||
await db.charts.iidx.insert(GetKTDataJSON("./tachi/tachi-charts-iidx.json"));
|
||||
await db.songs.iidx.insert(GetKTDataJSON("./tachi/tachi-songs-iidx.json") as Array<SongDocument<"iidx">>);
|
||||
await db.charts.iidx.insert(GetKTDataJSON("./tachi/tachi-charts-iidx.json") as Array<ChartDocument<"iidx:DP"|"iidx:SP">>);
|
||||
|
||||
await CreateFolderChartLookup(folder, true);
|
||||
|
||||
|
||||
+10
-4
@@ -1,5 +1,5 @@
|
||||
import db from "external/mongo/db";
|
||||
import { PBScoreDocument } from "tachi-common";
|
||||
import { ChartDocument, PBScoreDocument } from "tachi-common";
|
||||
import t from "tap";
|
||||
import mockApi from "test-utils/mock-api";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
@@ -89,16 +89,22 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/pbs", (t) => {
|
||||
t.test("Should search a user's personal bests.", async (t) => {
|
||||
const mockPBs: PBScoreDocument[] = [];
|
||||
|
||||
const charts = GetKTDataJSON("./tachi/tachi-charts-iidx.json");
|
||||
const charts = GetKTDataJSON("./tachi/tachi-charts-iidx.json") as Array<ChartDocument>;
|
||||
|
||||
for (let i = 0; i < 200; i++) {
|
||||
let chart = charts[i];
|
||||
|
||||
if (!chart) {
|
||||
return t.fail(`Not enough charts in tachi-charts-iidx.json to mock pb data? Failed at index ${i}.`);
|
||||
}
|
||||
|
||||
mockPBs.push({
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
isPrimary: true,
|
||||
chartID: charts[i].chartID,
|
||||
songID: charts[i].songID,
|
||||
chartID: chart.chartID,
|
||||
songID: chart.songID,
|
||||
calculatedData: {
|
||||
ktLampRating: i,
|
||||
},
|
||||
|
||||
+5
-1
@@ -43,7 +43,11 @@ router.put(
|
||||
rivalIDs: [p.isPositiveNonZeroInteger],
|
||||
}),
|
||||
async (req, res) => {
|
||||
const rivalIDs: Array<integer> = req.body.rivalIDs;
|
||||
const body = req.safeBody as {
|
||||
rivalIDs: Array<integer>;
|
||||
};
|
||||
|
||||
const rivalIDs = body.rivalIDs;
|
||||
const { user, game, playtype } = GetUGPT(req);
|
||||
|
||||
const result = await SetRivals(user.id, game, playtype, rivalIDs);
|
||||
|
||||
+10
-4
@@ -1,5 +1,5 @@
|
||||
import db from "external/mongo/db";
|
||||
import { ScoreDocument } from "tachi-common";
|
||||
import { ChartDocument, ScoreDocument } from "tachi-common";
|
||||
import t from "tap";
|
||||
import { mkFakeScoreIIDXSP, mkFakeScoreSDVX } from "test-utils/misc";
|
||||
import mockApi from "test-utils/mock-api";
|
||||
@@ -88,17 +88,23 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/scores", (t) => {
|
||||
t.test("Should search a user's scores.", async (t) => {
|
||||
const mockScores: ScoreDocument[] = [];
|
||||
|
||||
const charts = GetKTDataJSON("./tachi/tachi-charts-iidx.json");
|
||||
const charts = GetKTDataJSON("./tachi/tachi-charts-iidx.json") as Array<ChartDocument<"iidx:DP"|"iidx:SP">>;
|
||||
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const chart = charts[i];
|
||||
|
||||
if (!chart) {
|
||||
return t.fail(`tachi-charts-iidx.json doesn't have enough mock data for testing. Needed atleast 200 entries, but failed to retrieve one at index ${i}.`);
|
||||
}
|
||||
|
||||
mockScores.push({
|
||||
scoreID: i.toString(),
|
||||
userID: 1,
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
isPrimary: true,
|
||||
chartID: charts[i].chartID,
|
||||
songID: charts[i].songID,
|
||||
chartID: chart.chartID,
|
||||
songID: chart.songID,
|
||||
calculatedData: {
|
||||
ktLampRating: i,
|
||||
},
|
||||
|
||||
+23
-16
@@ -8,6 +8,7 @@ import { GetGamePTConfig } from "tachi-common";
|
||||
import { FormatPrError, optNull } from "utils/prudence";
|
||||
import { GetUGPT } from "utils/req-tachi-data";
|
||||
import { FormatUserDoc } from "utils/user";
|
||||
import type { UGPTSettings } from "tachi-common";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -47,7 +48,7 @@ router.patch(
|
||||
};
|
||||
}
|
||||
|
||||
const err = p(req.body, {
|
||||
const err = p(req.safeBody, {
|
||||
preferredScoreAlg: p.optional(p.nullable(p.isIn(gptConfig.scoreRatingAlgs))),
|
||||
preferredSessionAlg: p.optional(p.nullable(p.isIn(gptConfig.sessionRatingAlgs))),
|
||||
preferredProfileAlg: p.optional(p.nullable(p.isIn(gptConfig.profileRatingAlgs))),
|
||||
@@ -65,43 +66,49 @@ router.patch(
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof req.body.defaultTable === "string") {
|
||||
const body = req.safeBody as Partial<UGPTSettings["preferences"]>;
|
||||
|
||||
if (typeof body.defaultTable === "string") {
|
||||
const table = await db.tables.findOne({
|
||||
game,
|
||||
playtype,
|
||||
tableID: req.body.defaultTable,
|
||||
tableID: body.defaultTable,
|
||||
});
|
||||
|
||||
if (!table) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `The table (${req.body.defaultTable}) does not exist (and therefore cannot be set as a default).`,
|
||||
description: `The table (${body.defaultTable}) does not exist (and therefore cannot be set as a default).`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updateQuery: Record<string, string> = {};
|
||||
const updateQuery: Record<string, string | null> = {};
|
||||
|
||||
// @warning Slightly icky dynamic prop assignment instead of copypasta.
|
||||
for (const key of ["Score", "Session", "Profile"]) {
|
||||
const k = `preferred${key}Alg`;
|
||||
for (const key of ["Score", "Session", "Profile"] as const) {
|
||||
const k = `preferred${key}Alg` as const;
|
||||
|
||||
if (req.body[k] !== undefined) {
|
||||
updateQuery[`preferences.${k}`] = req.body[k];
|
||||
const value = body[k];
|
||||
|
||||
if (value !== undefined) {
|
||||
updateQuery[`preferences.${k}`] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.scoreBucket !== undefined) {
|
||||
updateQuery[`preferences.scoreBucket`] = req.body.scoreBucket;
|
||||
if (body.scoreBucket !== undefined) {
|
||||
updateQuery[`preferences.scoreBucket`] = body.scoreBucket;
|
||||
}
|
||||
|
||||
if (req.body.defaultTable !== undefined) {
|
||||
updateQuery[`preferences.defaultTable`] = req.body.defaultTable;
|
||||
if (body.defaultTable !== undefined) {
|
||||
updateQuery[`preferences.defaultTable`] = body.defaultTable;
|
||||
}
|
||||
|
||||
if (req.body.gameSpecific) {
|
||||
for (const key in req.body.gameSpecific) {
|
||||
updateQuery[`preferences.gameSpecific.${key}`] = req.body.gameSpecific[key];
|
||||
if (body.gameSpecific) {
|
||||
for (const [key, value] of Object.entries(body.gameSpecific)) {
|
||||
// @ts-expect-error This is a very hacky way of applying changes.
|
||||
// However, we know this to be correct, so we're just going to ignore it.
|
||||
updateQuery[`preferences.gameSpecific.${key}`] = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -167,21 +167,21 @@ router.put("/", RequireAuthedAsUser, RequirePermissions("customise_profile"), as
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
if (!Array.isArray(req.body)) {
|
||||
if (!Array.isArray(req.safeBody)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `No stats provided, or was not an array.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.length > 6) {
|
||||
if (req.safeBody.length > 6) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `You are only allowed 6 stats at once.`,
|
||||
});
|
||||
}
|
||||
|
||||
for (const stat of req.body) {
|
||||
for (const stat of req.safeBody) {
|
||||
let err;
|
||||
|
||||
if (stat?.mode === "chart") {
|
||||
@@ -275,7 +275,7 @@ router.put("/", RequireAuthedAsUser, RequirePermissions("customise_profile"), as
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
"preferences.stats": req.body,
|
||||
"preferences.stats": req.safeBody,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
+4
@@ -666,6 +666,10 @@ t.test("GET /api/v1/users/:userID/games/:game/:playtype/targets/goals/:goalID",
|
||||
await db["goal-subs"].insert(IIDXSPMilestoneGoalSubs);
|
||||
await db.milestones.insert(TestingIIDXSPMilestone);
|
||||
|
||||
if (!IIDXSPMilestoneGoalSubs[0] || !IIDXSPMilestoneGoals[0]) {
|
||||
throw new Error(`Expected atleast one milestone goal or sub to work with?`);
|
||||
}
|
||||
|
||||
const res = await mockApi.get(
|
||||
`/api/v1/users/1/games/iidx/SP/targets/goals/${IIDXSPMilestoneGoalSubs[0].goalID}`
|
||||
);
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
const data = req.body as GoalCreationBody;
|
||||
const data = req.safeBody as GoalCreationBody;
|
||||
|
||||
let goal;
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import { RequireSelfRequestFromUser } from "../../middleware";
|
||||
import { Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import { SYMBOL_TACHI_DATA } from "lib/constants/tachi";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import prValidate from "server/middleware/prudence-validate";
|
||||
import { RequireKamaitachi } from "server/middleware/type-require";
|
||||
import { IsNonEmptyString } from "utils/misc";
|
||||
import { GetArcAuth } from "utils/queries/auth";
|
||||
import { GetTachiData } from "utils/req-tachi-data";
|
||||
import { FormatUserDoc } from "utils/user";
|
||||
@@ -46,7 +46,12 @@ router.get("/", async (req, res) => {
|
||||
router.patch("/", prValidate({ iidx: "*?string", sdvx: "*?string" }), async (req, res) => {
|
||||
const user = GetTachiData(req, "requestedUser");
|
||||
|
||||
if (Object.keys(req.body).length === 0) {
|
||||
const body = req.safeBody as {
|
||||
iidx?: string | null;
|
||||
sdvx?: string | null;
|
||||
};
|
||||
|
||||
if (Object.keys(body).length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `Invalid request to modify nothing.`,
|
||||
@@ -63,7 +68,9 @@ router.patch("/", prValidate({ iidx: "*?string", sdvx: "*?string" }), async (req
|
||||
for (const key of ["iidx", "sdvx"] as const) {
|
||||
const importType = `api/arc-${key}` as const;
|
||||
|
||||
if (req.body[key] === null) {
|
||||
const value = body[key];
|
||||
|
||||
if (value === null) {
|
||||
logger.info(`User ${FormatUserDoc(user)} removed ARC integration for ${importType}.`);
|
||||
|
||||
await db["arc-saved-profiles"].remove(
|
||||
@@ -75,7 +82,7 @@ router.patch("/", prValidate({ iidx: "*?string", sdvx: "*?string" }), async (req
|
||||
single: true,
|
||||
}
|
||||
);
|
||||
} else if (req.body[key]) {
|
||||
} else if (IsNonEmptyString(value)) {
|
||||
if (existingData[key]) {
|
||||
logger.info(`User updated ARC integration for ${importType}.`);
|
||||
await db["arc-saved-profiles"].update(
|
||||
@@ -85,7 +92,7 @@ router.patch("/", prValidate({ iidx: "*?string", sdvx: "*?string" }), async (req
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
accountID: req.body[key],
|
||||
accountID: value,
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -94,7 +101,7 @@ router.patch("/", prValidate({ iidx: "*?string", sdvx: "*?string" }), async (req
|
||||
await db["arc-saved-profiles"].insert({
|
||||
userID: user.id,
|
||||
forImportType: importType,
|
||||
accountID: req.body[key],
|
||||
accountID: value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { RequireSelfRequestFromUser } from "../../middleware";
|
||||
import { Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import { SYMBOL_TACHI_DATA } from "lib/constants/tachi";
|
||||
import prValidate from "server/middleware/prudence-validate";
|
||||
import { RequireKamaitachi } from "server/middleware/type-require";
|
||||
import { DeleteUndefinedProps } from "utils/misc";
|
||||
@@ -44,7 +43,12 @@ router.patch(
|
||||
"/settings",
|
||||
prValidate({ cards: optNull(["string"]), forceStaticImport: "*?boolean" }),
|
||||
async (req, res) => {
|
||||
if (req.body.cards && req.body.cards.length > 6) {
|
||||
const body = req.safeBody as {
|
||||
cards?: Array<string> | null;
|
||||
forceStaticImport?: boolean | null;
|
||||
};
|
||||
|
||||
if (body.cards && body.cards.length > 6) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
description: `You cannot have more than 6 card filters at once.`,
|
||||
@@ -53,7 +57,7 @@ router.patch(
|
||||
|
||||
const user = GetTachiData(req, "requestedUser");
|
||||
|
||||
const modifyDocument = req.body;
|
||||
const modifyDocument = req.safeBody;
|
||||
|
||||
DeleteUndefinedProps(modifyDocument);
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ router.post(
|
||||
const user = GetTachiData(req, "requestedUser");
|
||||
const kaiType = NotNullish(req.params.kaiType).toUpperCase() as "EAG" | "FLO" | "MIN";
|
||||
|
||||
const body = req.body as {
|
||||
const body = req.safeBody as {
|
||||
code: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ router.patch(
|
||||
async (req, res) => {
|
||||
const user = NotNullish(req[SYMBOL_TACHI_DATA]?.requestedUser);
|
||||
|
||||
const body = req.body as UserPatchBody;
|
||||
const body = req.safeBody as UserPatchBody;
|
||||
|
||||
if (Object.keys(body).length === 0) {
|
||||
return res.status(400).json({
|
||||
@@ -311,7 +311,7 @@ router.post(
|
||||
"!oldPassword": ValidatePassword,
|
||||
}),
|
||||
async (req, res) => {
|
||||
const body = req.body as {
|
||||
const body = req.safeBody as {
|
||||
"!password": string;
|
||||
"!oldPassword": string;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { RequireSelfRequestFromUser } from "../middleware";
|
||||
import { Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import { SYMBOL_TACHI_DATA } from "lib/constants/tachi";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import prValidate from "server/middleware/prudence-validate";
|
||||
import { DeleteUndefinedProps } from "utils/misc";
|
||||
import { GetTachiData } from "utils/req-tachi-data";
|
||||
import { FormatUserDoc, GetSettingsForUser } from "utils/user";
|
||||
|
||||
@@ -61,16 +59,14 @@ router.patch(
|
||||
async (req, res) => {
|
||||
const user = GetTachiData(req, "requestedUser");
|
||||
|
||||
const preferences = {
|
||||
invisible: req.body.invisible,
|
||||
developerMode: req.body.developerMode,
|
||||
contentiousContent: req.body.contentiousContent,
|
||||
advancedMode: req.body.advancedMode,
|
||||
deletableScores: req.body.deletableScores,
|
||||
const preferences = req.safeBody as {
|
||||
invisible?: boolean;
|
||||
developerMode?: boolean;
|
||||
contentiousContent?: boolean;
|
||||
advancedMode?: boolean;
|
||||
deletableScores?: boolean;
|
||||
};
|
||||
|
||||
DeleteUndefinedProps(preferences);
|
||||
|
||||
if (Object.keys(preferences).length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
|
||||
@@ -13,10 +13,10 @@ router.use(RequirePermissions("submit_score"));
|
||||
*/
|
||||
router.post("/score/submit", async (req, res) => {
|
||||
const responseData = await ExpressWrappedScoreImportMain(
|
||||
req[SYMBOL_TACHI_API_AUTH]!.userID!,
|
||||
req[SYMBOL_TACHI_API_AUTH].userID!,
|
||||
false,
|
||||
"ir/barbatos",
|
||||
[req.body]
|
||||
[req.safeBody]
|
||||
);
|
||||
|
||||
return res.status(responseData.statusCode).json(responseData.body);
|
||||
|
||||
@@ -2,11 +2,9 @@ import deepmerge from "deepmerge";
|
||||
import { PBScoreDocument } from "tachi-common";
|
||||
import t from "tap";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { GetKTDataJSON } from "test-utils/test-data";
|
||||
import { BMSGazerChart, GetKTDataJSON } from "test-utils/test-data";
|
||||
import { TachiScoreDataToBeatorajaFormat } from "./convert-scores";
|
||||
|
||||
const gazerChart = GetKTDataJSON("./tachi/bms-gazer-chart.json");
|
||||
|
||||
const pbScore = {
|
||||
composedFrom: {
|
||||
lampPB: "mock_lampPB",
|
||||
@@ -18,7 +16,7 @@ const pbScore = {
|
||||
},
|
||||
playtype: "7K",
|
||||
scoreMeta: {},
|
||||
chartID: gazerChart.chartID,
|
||||
chartID: BMSGazerChart.chartID,
|
||||
userID: 1,
|
||||
} as unknown as PBScoreDocument<"bms:7K" | "bms:14K">;
|
||||
|
||||
@@ -28,9 +26,9 @@ t.test("#TachiScoreDataToBeatorajaFormat", (t) => {
|
||||
t.test("Should convert score.", (t) => {
|
||||
const res = TachiScoreDataToBeatorajaFormat(
|
||||
pbScore,
|
||||
gazerChart.data.hashSHA256,
|
||||
BMSGazerChart.data.hashSHA256,
|
||||
"",
|
||||
gazerChart.data.notecount,
|
||||
BMSGazerChart.data.notecount,
|
||||
0
|
||||
);
|
||||
|
||||
@@ -71,9 +69,9 @@ t.test("#TachiScoreDataToBeatorajaFormat", (t) => {
|
||||
t.test("Should fake epg/egr data if the score doesn't have it..", (t) => {
|
||||
const res = TachiScoreDataToBeatorajaFormat(
|
||||
deepmerge(pbScore, { scoreData: { score: 999 } }),
|
||||
gazerChart.data.hashSHA256,
|
||||
BMSGazerChart.data.hashSHA256,
|
||||
"",
|
||||
gazerChart.data.notecount,
|
||||
BMSGazerChart.data.notecount,
|
||||
0
|
||||
);
|
||||
|
||||
@@ -114,9 +112,9 @@ t.test("#TachiScoreDataToBeatorajaFormat", (t) => {
|
||||
t.test("Should emplace username if requestingUserID is not the pbscore owner", (t) => {
|
||||
const res = TachiScoreDataToBeatorajaFormat(
|
||||
pbScore,
|
||||
gazerChart.data.hashSHA256,
|
||||
BMSGazerChart.data.hashSHA256,
|
||||
"test_zkldi",
|
||||
gazerChart.data.notecount,
|
||||
BMSGazerChart.data.notecount,
|
||||
0
|
||||
);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { PublicUserDocument } from "tachi-common";
|
||||
import t from "tap";
|
||||
import mockApi from "test-utils/mock-api";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { GetKTDataJSON } from "test-utils/test-data";
|
||||
import { GetKTDataJSON, MockBeatorajaBMSScore, MockBeatorajaPMSScore } from "test-utils/test-data";
|
||||
|
||||
t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
t.beforeEach(ResetDBState);
|
||||
@@ -20,15 +20,12 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
})
|
||||
);
|
||||
|
||||
const bmsScoreReq = GetKTDataJSON("./beatoraja/base.json");
|
||||
const pmsScoreReq = GetKTDataJSON("./beatoraja/pms-base.json");
|
||||
|
||||
t.test("Should import a valid BMS score.", async (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/beatoraja/submit-score")
|
||||
.set("X-TachiIR-Version", "v2.0.0")
|
||||
.set("Authorization", "Bearer mock_token")
|
||||
.send(bmsScoreReq);
|
||||
.send(MockBeatorajaBMSScore);
|
||||
|
||||
t.equal(res.status, 200);
|
||||
|
||||
@@ -67,7 +64,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
.post("/ir/beatoraja/submit-score")
|
||||
.set("X-TachiIR-Version", "v2.0.0")
|
||||
.set("Authorization", "Bearer mock_token")
|
||||
.send(pmsScoreReq);
|
||||
.send(MockBeatorajaPMSScore);
|
||||
|
||||
t.equal(res.status, 200);
|
||||
|
||||
@@ -106,7 +103,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
.post("/ir/beatoraja/submit-score")
|
||||
.set("X-TachiIR-Version", "v2.0.0")
|
||||
.set("Authorization", "Bearer mock_token")
|
||||
.send(deepmerge(pmsScoreReq, { score: { deviceType: "KEYBOARD" } }));
|
||||
.send(deepmerge(MockBeatorajaPMSScore, { score: { deviceType: "KEYBOARD" } }));
|
||||
|
||||
t.equal(res.status, 200);
|
||||
|
||||
@@ -145,7 +142,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
.post("/ir/beatoraja/submit-score")
|
||||
.set("X-TachiIR-Version", "v2.0.0")
|
||||
.set("Authorization", "Bearer mock_token")
|
||||
.send(deepmerge(bmsScoreReq, { client: "INVALID" }));
|
||||
.send(deepmerge(MockBeatorajaBMSScore, { client: "INVALID" }));
|
||||
|
||||
t.equal(res.status, 400);
|
||||
|
||||
@@ -160,7 +157,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
.post("/ir/beatoraja/submit-score")
|
||||
.set("X-TachiIR-Version", "v2.0.0")
|
||||
.set("Authorization", "Bearer mock_token")
|
||||
.send(deepmerge(bmsScoreReq, { client: "beatoraja 0.8.0" }));
|
||||
.send(deepmerge(MockBeatorajaBMSScore, { client: "beatoraja 0.8.0" }));
|
||||
|
||||
t.equal(res.status, 400);
|
||||
|
||||
@@ -175,7 +172,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
.post("/ir/beatoraja/submit-score")
|
||||
.set("X-TachiIR-Version", "v2.0.0")
|
||||
.set("Authorization", "Bearer mock_token")
|
||||
.send(deepmerge(pmsScoreReq, { client: "LR2oraja 0.8.0" }));
|
||||
.send(deepmerge(MockBeatorajaPMSScore, { client: "LR2oraja 0.8.0" }));
|
||||
|
||||
t.equal(res.status, 400);
|
||||
|
||||
@@ -190,7 +187,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
.post("/ir/beatoraja/submit-score")
|
||||
.set("X-TachiIR-Version", "v2.0.0")
|
||||
.set("Authorization", "Bearer mock_token")
|
||||
.send(deepmerge(bmsScoreReq, { score: { exscore: -1 } }));
|
||||
.send(deepmerge(MockBeatorajaBMSScore, { score: { exscore: -1 } }));
|
||||
|
||||
t.equal(res.status, 400);
|
||||
|
||||
@@ -205,7 +202,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
.post("/ir/beatoraja/submit-score")
|
||||
.set("X-TachiIR-Version", "v2.0.0")
|
||||
.set("Authorization", "Bearer mock_token")
|
||||
.send(deepmerge(bmsScoreReq, { chart: { title: null } }));
|
||||
.send(deepmerge(MockBeatorajaBMSScore, { chart: { title: null } }));
|
||||
|
||||
t.equal(res.status, 400);
|
||||
|
||||
@@ -221,7 +218,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
.set("X-TachiIR-Version", "v2.0.0")
|
||||
.set("Authorization", "Bearer mock_token")
|
||||
.send(
|
||||
deepmerge(bmsScoreReq, {
|
||||
deepmerge(MockBeatorajaBMSScore, {
|
||||
chart: { sha256: "new_chart", md5: "new_md5" },
|
||||
score: { sha256: "new_chart", md5: "new_md5" },
|
||||
})
|
||||
@@ -282,7 +279,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
.set("X-TachiIR-Version", "v2.0.0")
|
||||
.set("Authorization", "Bearer mock_token")
|
||||
.send(
|
||||
deepmerge(bmsScoreReq, {
|
||||
deepmerge(MockBeatorajaBMSScore, {
|
||||
chart: { sha256: "new_chart", md5: "new_md5" },
|
||||
score: { sha256: "new_chart", md5: "new_md5" },
|
||||
})
|
||||
@@ -295,7 +292,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
.set("X-TachiIR-Version", "v2.0.0")
|
||||
.set("Authorization", "Bearer token2")
|
||||
.send(
|
||||
deepmerge(bmsScoreReq, {
|
||||
deepmerge(MockBeatorajaBMSScore, {
|
||||
chart: { sha256: "new_chart", md5: "new_md5" },
|
||||
score: { sha256: "new_chart", md5: "new_md5" },
|
||||
})
|
||||
@@ -314,7 +311,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
.set("X-TachiIR-Version", "v2.0.0")
|
||||
.set("Authorization", "Bearer token3")
|
||||
.send(
|
||||
deepmerge(bmsScoreReq, {
|
||||
deepmerge(MockBeatorajaBMSScore, {
|
||||
chart: { sha256: "new_chart", md5: "new_md5" },
|
||||
score: { sha256: "new_chart", md5: "new_md5" },
|
||||
})
|
||||
@@ -347,7 +344,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/beatoraja/submit-score")
|
||||
.set("X-TachiIR-Version", "v2.0.0")
|
||||
.send(bmsScoreReq);
|
||||
.send(MockBeatorajaBMSScore);
|
||||
|
||||
t.equal(res.status, 401);
|
||||
|
||||
@@ -360,7 +357,7 @@ t.test("POST /ir/beatoraja/submit-score", (t) => {
|
||||
.set("X-TachiIR-Version", "v2.0.0")
|
||||
.set("Authorization", "Bearer invalid_token")
|
||||
|
||||
.send(bmsScoreReq);
|
||||
.send(MockBeatorajaBMSScore);
|
||||
|
||||
t.equal(res.status, 401);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ router.post("/submit-score", RequireNotGuest, async (req, res) => {
|
||||
const userID = NotNullish(req[SYMBOL_TACHI_API_AUTH].userID);
|
||||
|
||||
const importRes = await ExpressWrappedScoreImportMain(userID, false, "ir/beatoraja", [
|
||||
req.body,
|
||||
req.safeBody,
|
||||
userID,
|
||||
]);
|
||||
|
||||
@@ -43,7 +43,7 @@ router.post("/submit-score", RequireNotGuest, async (req, res) => {
|
||||
// the chart and score values were atleast typed correctly
|
||||
// and can afford to make this assertion.
|
||||
if (type === "KTDataNotFound") {
|
||||
const { chart } = req.body as { chart: BeatorajaChart };
|
||||
const { chart } = req.safeBody as { chart: BeatorajaChart };
|
||||
|
||||
const orphanInfo: { userIDs: Array<integer> } | null = await db[
|
||||
"orphan-chart-queue"
|
||||
@@ -56,7 +56,7 @@ router.post("/submit-score", RequireNotGuest, async (req, res) => {
|
||||
|
||||
if (!orphanInfo) {
|
||||
logger.warn(`Chart '${chart.sha256}' got KTDataNotFound, but was not orphaned?`, {
|
||||
body: req.body as unknown,
|
||||
body: req.safeBody as unknown,
|
||||
});
|
||||
|
||||
return res.status(400).json({
|
||||
@@ -192,7 +192,7 @@ router.post(
|
||||
}),
|
||||
RequireNotGuest,
|
||||
async (req, res) => {
|
||||
const body = req.body as {
|
||||
const body = req.safeBody as {
|
||||
course: {
|
||||
charts: Array<{ md5: string }>;
|
||||
constraint: Array<"GAUGE_LR2" | "LN" | "MIRROR">;
|
||||
|
||||
@@ -5,7 +5,7 @@ import t from "tap";
|
||||
import { CreateFakeAuthCookie } from "test-utils/fake-auth";
|
||||
import mockApi from "test-utils/mock-api";
|
||||
import ResetDBState from "test-utils/resets";
|
||||
import { GetKTDataJSON } from "test-utils/test-data";
|
||||
import { FakeChunitachiBatchManual, FakeSmallBatchManual, GetKTDataJSON } from "test-utils/test-data";
|
||||
|
||||
t.test("POST /ir/direct-manual/import", async (t) => {
|
||||
const cookie = await CreateFakeAuthCookie(mockApi);
|
||||
@@ -38,7 +38,7 @@ t.test("POST /ir/direct-manual/import", async (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/direct-manual/import")
|
||||
.set("Cookie", cookie)
|
||||
.send(GetKTDataJSON("./batch-manual/small-file.json"));
|
||||
.send(FakeSmallBatchManual);
|
||||
|
||||
t.equal(res.body.success, true, "Should be successful");
|
||||
|
||||
@@ -62,7 +62,7 @@ t.test("POST /ir/direct-manual/import", async (t) => {
|
||||
t.test("Should require authentication.", async (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/direct-manual/import")
|
||||
.send(GetKTDataJSON("./batch-manual/small-file.json"));
|
||||
.send(FakeSmallBatchManual);
|
||||
|
||||
t.equal(res.statusCode, 401);
|
||||
|
||||
@@ -73,7 +73,7 @@ t.test("POST /ir/direct-manual/import", async (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/direct-manual/import")
|
||||
.set("Authorization", "Bearer invalid_token")
|
||||
.send(GetKTDataJSON("./batch-manual/small-file.json"));
|
||||
.send(FakeSmallBatchManual);
|
||||
|
||||
t.equal(res.statusCode, 401);
|
||||
|
||||
@@ -90,13 +90,11 @@ t.test("POST /ir/direct-manual/import", async (t) => {
|
||||
})
|
||||
);
|
||||
|
||||
const chunitachiBody = GetKTDataJSON("./batch-manual/chunitachi.json");
|
||||
|
||||
t.test("Should work for CHUNITACHI requests", async (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/direct-manual/import")
|
||||
.set("Authorization", `Bearer mock_token`)
|
||||
.send(chunitachiBody);
|
||||
.send(FakeChunitachiBatchManual);
|
||||
|
||||
t.equal(res.body.success, true, "Should be successful");
|
||||
|
||||
@@ -121,7 +119,7 @@ t.test("POST /ir/direct-manual/import", async (t) => {
|
||||
});
|
||||
|
||||
t.test("Should require authentication.", async (t) => {
|
||||
const res = await mockApi.post("/ir/direct-manual/import").send(chunitachiBody);
|
||||
const res = await mockApi.post("/ir/direct-manual/import").send(FakeChunitachiBatchManual);
|
||||
|
||||
t.equal(res.statusCode, 401);
|
||||
|
||||
@@ -132,7 +130,7 @@ t.test("POST /ir/direct-manual/import", async (t) => {
|
||||
const res = await mockApi
|
||||
.post("/ir/direct-manual/import")
|
||||
.set("Authorization", "Bearer invalid_token")
|
||||
.send(chunitachiBody);
|
||||
.send(FakeChunitachiBatchManual);
|
||||
|
||||
t.equal(res.statusCode, 401);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ router.post(
|
||||
userID: req[SYMBOL_TACHI_API_AUTH].userID!,
|
||||
userIntent,
|
||||
importType: "ir/direct-manual",
|
||||
parserArguments: [req.body],
|
||||
parserArguments: [req.safeBody],
|
||||
};
|
||||
|
||||
// Fire the score import, but make no guarantees about its state.
|
||||
@@ -51,7 +51,7 @@ router.post(
|
||||
req[SYMBOL_TACHI_API_AUTH].userID!,
|
||||
userIntent,
|
||||
"ir/direct-manual",
|
||||
[req.body]
|
||||
[req.safeBody]
|
||||
);
|
||||
|
||||
return res.status(importResponse.statusCode).json(importResponse.body);
|
||||
|
||||
@@ -184,7 +184,7 @@ const ValidateModelHeader: RequestHandler = (req, res, next) => {
|
||||
};
|
||||
|
||||
const ValidateCards: RequestHandler = async (req, res, next) => {
|
||||
const userID = req[SYMBOL_TACHI_API_AUTH]!.userID!;
|
||||
const userID = req[SYMBOL_TACHI_API_AUTH].userID!;
|
||||
|
||||
const cardFilters = await db["fer-settings"].findOne({ userID });
|
||||
|
||||
@@ -244,7 +244,7 @@ router.post("/profile/submit", RequireInf2ModelHeaderOrForceStatic, (req, res) =
|
||||
req[SYMBOL_TACHI_API_AUTH].userID!,
|
||||
false,
|
||||
"ir/fervidex-static",
|
||||
[req.body, headers]
|
||||
[req.safeBody, headers]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -273,7 +273,7 @@ router.post("/score/submit", ValidateModelHeader, async (req, res) => {
|
||||
req[SYMBOL_TACHI_API_AUTH].userID!,
|
||||
true,
|
||||
"ir/fervidex",
|
||||
[req.body, headers]
|
||||
[req.safeBody, headers]
|
||||
);
|
||||
|
||||
if (!responseData.body.success) {
|
||||
@@ -307,7 +307,7 @@ router.post(
|
||||
),
|
||||
ValidateModelHeader,
|
||||
async (req, res) => {
|
||||
const body = req.body as {
|
||||
const body = req.safeBody as {
|
||||
cleared: boolean;
|
||||
course_id: integer;
|
||||
play_style: 0 | 1;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper";
|
||||
import { ParseEA3SoftID } from "utils/ea3id";
|
||||
import { IsNullishOrEmptyStr } from "utils/misc";
|
||||
import type { RequestHandler } from "express";
|
||||
|
||||
const router: Router = Router({ mergeParams: true });
|
||||
@@ -13,9 +14,9 @@ const logger = CreateLogCtx(__filename);
|
||||
const ValidateHeaders: RequestHandler = (req, res, next) => {
|
||||
const agent = req.header("User-Agent");
|
||||
|
||||
if (!agent) {
|
||||
if (IsNullishOrEmptyStr(agent)) {
|
||||
logger.debug(
|
||||
`Rejected KsHook client with no agent from user ${req[SYMBOL_TACHI_API_AUTH].userID!}.`
|
||||
`Rejected KsHook client with no agent from user ${req[SYMBOL_TACHI_API_AUTH].userID}.`
|
||||
);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@@ -25,9 +26,7 @@ const ValidateHeaders: RequestHandler = (req, res, next) => {
|
||||
|
||||
if (!agent.startsWith("kshook/")) {
|
||||
logger.info(
|
||||
`Rejected KsHook client with invalid agent ${agent} from user ${req[
|
||||
SYMBOL_TACHI_API_AUTH
|
||||
].userID!}.`
|
||||
`Rejected KsHook client with invalid agent ${agent} from user ${req[SYMBOL_TACHI_API_AUTH].userID}.`
|
||||
);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@@ -40,9 +39,9 @@ const ValidateHeaders: RequestHandler = (req, res, next) => {
|
||||
|
||||
const softID = req.header("X-Software-Model");
|
||||
|
||||
if (!softID) {
|
||||
if (IsNullishOrEmptyStr(softID)) {
|
||||
logger.debug(
|
||||
`received request without X-Software-Model from ${req[SYMBOL_TACHI_API_AUTH].userID!}.`
|
||||
`received request without X-Software-Model from ${req[SYMBOL_TACHI_API_AUTH].userID}.`
|
||||
);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@@ -63,7 +62,7 @@ const ValidateHeaders: RequestHandler = (req, res, next) => {
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.info(`Invalid softID from ${req[SYMBOL_TACHI_API_AUTH].userID!}.`, { err });
|
||||
logger.info(`Invalid softID from ${req[SYMBOL_TACHI_API_AUTH].userID}.`, { err });
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: `Invalid X-Software-Model.`,
|
||||
@@ -85,7 +84,7 @@ router.post("/score/save", async (req, res) => {
|
||||
req[SYMBOL_TACHI_API_AUTH].userID!,
|
||||
true,
|
||||
"ir/kshook-sv6c",
|
||||
[req.body]
|
||||
[req.safeBody]
|
||||
);
|
||||
|
||||
if (!responseData.body.success) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Router } from "express";
|
||||
import db from "external/mongo/db";
|
||||
import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi";
|
||||
import { ExpressWrappedScoreImportMain } from "lib/score-import/framework/express-wrapper";
|
||||
import { PR_LR2Hook } from "lib/score-import/import-types/ir/lr2hook/parser";
|
||||
import { PR_LR2Hook as PR_LR2_HOOK } from "lib/score-import/import-types/ir/lr2hook/parser";
|
||||
import { RequirePermissions } from "server/middleware/auth";
|
||||
import prValidate from "server/middleware/prudence-validate";
|
||||
import { UpdateClassIfGreater } from "utils/class";
|
||||
@@ -22,7 +22,7 @@ router.post("/import", async (req, res) => {
|
||||
req[SYMBOL_TACHI_API_AUTH].userID!,
|
||||
false,
|
||||
"ir/lr2hook",
|
||||
[req.body]
|
||||
[req.safeBody]
|
||||
);
|
||||
|
||||
return res.status(importRes.statusCode).json(importRes.body);
|
||||
@@ -33,11 +33,13 @@ router.post("/import", async (req, res) => {
|
||||
*
|
||||
* @name POST /ir/lr2hook/import/course
|
||||
*/
|
||||
router.post("/import/course", prValidate(PR_LR2Hook), async (req, res) => {
|
||||
router.post("/import/course", prValidate(PR_LR2_HOOK), async (req, res) => {
|
||||
// notably, courses in LR2 are actually identical to scores. They have all
|
||||
// the same fields in all the same ways. The only significant difference is that
|
||||
// the md5 field is 4 fields conjoined, rather than just one.
|
||||
const score: LR2HookScore = req.body;
|
||||
|
||||
// This type assertion is safe due to the prValidate call above.
|
||||
const score = req.safeBody as unknown as LR2HookScore;
|
||||
|
||||
if (score.scoreData.notesPlayed !== score.scoreData.notesTotal) {
|
||||
return res.status(200).json({
|
||||
@@ -61,7 +63,7 @@ router.post("/import/course", prValidate(PR_LR2Hook), async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const userID = req[SYMBOL_TACHI_API_AUTH]!.userID!;
|
||||
const userID = req[SYMBOL_TACHI_API_AUTH].userID!;
|
||||
|
||||
const result = await UpdateClassIfGreater(
|
||||
userID,
|
||||
|
||||
@@ -4,7 +4,7 @@ import db from "external/mongo/db";
|
||||
import { CDNStoreOrOverwrite } from "lib/cdn/cdn";
|
||||
import { GetUSCIRReplayURL } from "lib/cdn/url-format";
|
||||
import { ONE_MEGABYTE } from "lib/constants/filesize";
|
||||
import { SYMBOL_TACHI_API_AUTH, SYMBOL_TACHI_DATA } from "lib/constants/tachi";
|
||||
import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi";
|
||||
import { USCIR_MAX_LEADERBOARD_N } from "lib/constants/usc-ir";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { AssertStrAsPositiveNonZeroInt } from "lib/score-import/framework/common/string-asserts";
|
||||
@@ -116,10 +116,19 @@ const RetrieveChart: RequestHandler = async (req, res, next) => {
|
||||
playtype: req.params.playtype as Playtypes["usc"],
|
||||
});
|
||||
|
||||
if (!chart) {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.NOT_FOUND,
|
||||
description: `This IR doesn't have any record data yet, or ${
|
||||
ServerConfig.USC_QUEUE_SIZE
|
||||
} ${
|
||||
ServerConfig.USC_QUEUE_SIZE === 1 ? "person has" : "people have"
|
||||
} not played the chart yet.`,
|
||||
});
|
||||
}
|
||||
|
||||
AssignToReqTachiData(req, {
|
||||
uscChartDoc: (chart ?? undefined) as
|
||||
| ChartDocument<"usc:Controller" | "usc:Keyboard">
|
||||
| undefined,
|
||||
uscChartDoc: chart as ChartDocument<"usc:Controller" | "usc:Keyboard">,
|
||||
});
|
||||
|
||||
next();
|
||||
@@ -131,15 +140,6 @@ const RetrieveChart: RequestHandler = async (req, res, next) => {
|
||||
* @name GET /ir/usc/:playtype/charts/:chartHash
|
||||
*/
|
||||
router.get("/charts/:chartHash", RetrieveChart, (req, res) => {
|
||||
const chart = GetTachiData(req, "uscChartDoc");
|
||||
|
||||
if (!chart) {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.NOT_FOUND,
|
||||
description: "This chart is not available on the IR yet, more people need to play it!",
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.SUCCESS,
|
||||
description: "This chart is tracked by the IR.",
|
||||
@@ -155,17 +155,6 @@ router.get("/charts/:chartHash", RetrieveChart, (req, res) => {
|
||||
router.get("/charts/:chartHash/record", RetrieveChart, async (req, res) => {
|
||||
const chart = GetTachiData(req, "uscChartDoc");
|
||||
|
||||
if (!chart) {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.NOT_FOUND,
|
||||
description: `This IR doesn't have any record data yet, or ${
|
||||
ServerConfig.USC_QUEUE_SIZE
|
||||
} ${
|
||||
ServerConfig.USC_QUEUE_SIZE === 1 ? "person has" : "people have"
|
||||
} not played the chart yet.`,
|
||||
});
|
||||
}
|
||||
|
||||
const serverRecord = (await db["personal-bests"].findOne({
|
||||
chartID: chart.chartID,
|
||||
"rankingData.rank": 1,
|
||||
@@ -195,17 +184,6 @@ router.get("/charts/:chartHash/record", RetrieveChart, async (req, res) => {
|
||||
router.get("/charts/:chartHash/leaderboard", RetrieveChart, async (req, res) => {
|
||||
const chart = GetTachiData(req, "uscChartDoc");
|
||||
|
||||
if (!chart) {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.NOT_FOUND,
|
||||
description: `This IR doesn't have any record data yet, or ${
|
||||
ServerConfig.USC_QUEUE_SIZE
|
||||
} ${
|
||||
ServerConfig.USC_QUEUE_SIZE === 1 ? "person has" : "people have"
|
||||
} not played the chart yet.`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!(typeof req.query.mode === "string" && ["best", "rivals"].includes(req.query.mode))) {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.BAD_REQ,
|
||||
@@ -285,7 +263,7 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => {
|
||||
const playtype = req.params.playtype as Playtypes["usc"];
|
||||
|
||||
const chartErr = p(
|
||||
req.body.chart,
|
||||
req.safeBody.chart,
|
||||
PR_USCIR_CHART_DOC,
|
||||
{},
|
||||
{
|
||||
@@ -301,17 +279,17 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const uscChart = req.body.chart as USCClientChart;
|
||||
const uscChart = req.safeBody.chart as USCClientChart;
|
||||
|
||||
const chartDoc = (await db.charts.usc.findOne({
|
||||
"data.hashSHA1": uscChart.chartHash,
|
||||
playtype,
|
||||
})) as ChartDocument<"usc:Controller" | "usc:Keyboard"> | null;
|
||||
|
||||
const userID = req[SYMBOL_TACHI_API_AUTH]!.userID!;
|
||||
const userID = req[SYMBOL_TACHI_API_AUTH].userID!;
|
||||
|
||||
const importRes = await ExpressWrappedScoreImportMain(userID, false, "ir/usc", [
|
||||
req.body,
|
||||
req.safeBody,
|
||||
uscChart.chartHash,
|
||||
playtype,
|
||||
]);
|
||||
@@ -332,20 +310,15 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => {
|
||||
|
||||
// If the import failed, AND the import failure WAS NOT that the chart didnt exist
|
||||
// report that error instead.
|
||||
if (importDoc.errors[0]?.type && importDoc.errors[0].type !== "KTDataNotFound") {
|
||||
logger.info(
|
||||
`USC Import Failed ${importDoc.errors[0].message ?? "with null error message?"}`,
|
||||
{
|
||||
importDoc,
|
||||
userID,
|
||||
}
|
||||
);
|
||||
if (importDoc.errors[0] && importDoc.errors[0].type !== "KTDataNotFound") {
|
||||
logger.info(`USC Import Failed ${importDoc.errors[0].message}`, {
|
||||
importDoc,
|
||||
userID,
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.BAD_REQ,
|
||||
description: `${importDoc.errors[0].type} ${
|
||||
importDoc.errors[0].message ?? "No Error Message"
|
||||
}`,
|
||||
description: `${importDoc.errors[0].type} ${importDoc.errors[0].message}`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -405,7 +378,7 @@ router.post(
|
||||
RequirePermissions("submit_score"),
|
||||
CreateMulterSingleUploadMiddleware("replay", ONE_MEGABYTE, logger, false),
|
||||
async (req, res) => {
|
||||
if (typeof req.body.identifier !== "string") {
|
||||
if (typeof req.safeBody.identifier !== "string") {
|
||||
return res.status(200).json({
|
||||
statusCode: STATUS_CODES.BAD_REQ,
|
||||
description: "No Identifier Provided.",
|
||||
@@ -425,8 +398,8 @@ router.post(
|
||||
// Otherwise, anyone could overwrite anyone elses
|
||||
// score replays!
|
||||
const correspondingScore = await db.scores.findOne({
|
||||
userID: req[SYMBOL_TACHI_API_AUTH]!.userID!,
|
||||
scoreID: req.body.identifier,
|
||||
userID: req[SYMBOL_TACHI_API_AUTH].userID!,
|
||||
scoreID: req.safeBody.identifier,
|
||||
game: "usc",
|
||||
});
|
||||
|
||||
|
||||
+33
-10
@@ -1,17 +1,21 @@
|
||||
// THIS IMPORT **MUST** GO HERE. DO NOT MOVE IT. IT MUST OCCUR BEFORE ANYTHING HAPPENS WITH EXPRESS
|
||||
// BUT AFTER EXPRESS IS IMPORTED.
|
||||
// eslint-disable-next-line import/order
|
||||
import express from "express";
|
||||
import "express-async-errors";
|
||||
|
||||
import { RequestLoggerMiddleware } from "./middleware/request-logger";
|
||||
import mainRouter from "./router/router";
|
||||
import connectRedis from "connect-redis";
|
||||
import express from "express";
|
||||
import expressSession from "express-session";
|
||||
import { RedisClient } from "external/redis/redis";
|
||||
import helmet from "helmet";
|
||||
import { SYMBOL_TACHI_API_AUTH } from "lib/constants/tachi";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { Environment, ServerConfig, TachiConfig } from "lib/setup/config";
|
||||
import { IsNonEmptyString, IsRecord } from "utils/misc";
|
||||
import type { Express } from "express";
|
||||
import "express-async-errors";
|
||||
import type { integer } from "tachi-common";
|
||||
import { IsNonEmptyString } from "utils/misc";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -53,9 +57,17 @@ if (Environment.nodeEnv !== "production" && IsNonEmptyString(ServerConfig.CLIENT
|
||||
bootInfo: true,
|
||||
});
|
||||
|
||||
// Note: we have to assign it here to make sure it doesn't get modified!
|
||||
// If we try and use ServerConfig.CLIENT_DEV_SERVER inside the callback, TS rightly
|
||||
// complains that this value might end up being mutated to null/undefined.
|
||||
//
|
||||
// Even though we don't do that,
|
||||
// we may aswell be correct about the whole thing.
|
||||
const clientDevServerLocation = ServerConfig.CLIENT_DEV_SERVER;
|
||||
|
||||
// Allow CORS requests from another server (since we have our dev server hosted separately).
|
||||
app.use((req, res, next) => {
|
||||
res.header("Access-Control-Allow-Origin", ServerConfig.CLIENT_DEV_SERVER!);
|
||||
res.header("Access-Control-Allow-Origin", clientDevServerLocation);
|
||||
res.header(
|
||||
"Access-Control-Allow-Headers",
|
||||
"Origin, X-Requested-With, Content-Type, Accept, X-User-Intent"
|
||||
@@ -103,11 +115,14 @@ app.use(express.json({ limit: "4mb" }));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
// Always mount an empty req body. We operate under the assumption that req.body is
|
||||
// always defined.
|
||||
if (req.method !== "GET" && !req.body) {
|
||||
// always defined as atleast an object.
|
||||
if (req.method !== "GET" && (typeof req.body !== "object" || req.body === null)) {
|
||||
req.body = {};
|
||||
}
|
||||
|
||||
// req.safeBody *is* just a type-safe req.body!
|
||||
req.safeBody = req.body as Record<string, unknown>;
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
@@ -143,8 +158,9 @@ interface ExpressJSONErr extends SyntaxError {
|
||||
message: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const MAIN_ERR_HANDLER: express.ErrorRequestHandler = (err, req, res, next) => {
|
||||
const MAIN_ERR_HANDLER: express.ErrorRequestHandler = (err, req, res, _next) => {
|
||||
logger.info(`MAIN_ERR_HANDLER hit by request.`, { url: req.originalUrl });
|
||||
|
||||
if (err instanceof SyntaxError) {
|
||||
const expErr: ExpressJSONErr = err as ExpressJSONErr;
|
||||
|
||||
@@ -159,14 +175,21 @@ const MAIN_ERR_HANDLER: express.ErrorRequestHandler = (err, req, res, next) => {
|
||||
// else, this isn't a JSON parsing error
|
||||
}
|
||||
|
||||
if (err.type === "entity.too.large") {
|
||||
const unknownErr = err as unknown;
|
||||
|
||||
if (IsRecord(unknownErr) && unknownErr.type === "entity.too.large") {
|
||||
return res.status(413).json({
|
||||
success: false,
|
||||
description: "Your request body was too large. The limit is 4MB.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.error("Fatal error propagated to server root? ", { err, route: req.route });
|
||||
logger.error("Fatal error propagated to server root? ", {
|
||||
err: unknownErr,
|
||||
url: req.originalUrl,
|
||||
authInfo: req[SYMBOL_TACHI_API_AUTH],
|
||||
});
|
||||
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
description: "A fatal internal server error has occured.",
|
||||
|
||||
@@ -23,7 +23,15 @@ export async function CreateFakeAuthCookie(mockApi: supertest.SuperTest<supertes
|
||||
throw res.body;
|
||||
}
|
||||
|
||||
return res.headers["set-cookie"] as Array<string>;
|
||||
const headers = res.headers as Record<string, Array<string>>;
|
||||
|
||||
const setCookieHeader: Array<string> | undefined = headers["set-cookie"];
|
||||
|
||||
if (setCookieHeader === undefined) {
|
||||
throw new Error(`Failed to login, no Set-Cookie returned in response?`);
|
||||
}
|
||||
|
||||
return setCookieHeader;
|
||||
}
|
||||
|
||||
// my local dev env hates this part because of pnpm
|
||||
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
FakeImport,
|
||||
FakeNotification,
|
||||
FakeOtherUser,
|
||||
HC511Goal,
|
||||
HC511UserGoal,
|
||||
TestingIIDXSPScore,
|
||||
TestingIIDXSPScorePB,
|
||||
TestingSDVXScore,
|
||||
@@ -10,6 +12,8 @@ import {
|
||||
import deepmerge from "deepmerge";
|
||||
import type {
|
||||
Game,
|
||||
GoalDocument,
|
||||
GoalSubscriptionDocument,
|
||||
ImportDocument,
|
||||
integer,
|
||||
NotificationDocument,
|
||||
@@ -39,7 +43,7 @@ export async function agta(ag: AsyncIterable<unknown> | Iterable<unknown>) {
|
||||
export function dmf<T extends object>(base: T, modifant: Partial<T>): T {
|
||||
return deepmerge(base, modifant, {
|
||||
// The new array should replace the former one, instead of joining them together.
|
||||
arrayMerge: (originalArray, newArray) => newArray,
|
||||
arrayMerge: (originalArray, newArray) => newArray as Array<unknown>,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,3 +95,11 @@ export function mkFakePBIIDXSP(modifant: Partial<PBScoreDocument<"iidx:SP">> = {
|
||||
export function mkFakeNotification(modifant: Partial<NotificationDocument> = {}) {
|
||||
return dmf(FakeNotification, modifant);
|
||||
}
|
||||
|
||||
export function mkFakeGoal(modifant: Partial<GoalDocument> = {}) {
|
||||
return dmf(HC511Goal, modifant);
|
||||
}
|
||||
|
||||
export function mkFakeGoalSub(modifant: Partial<GoalSubscriptionDocument> = {}) {
|
||||
return dmf(HC511UserGoal, modifant);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,17 @@
|
||||
"notecount": 2256,
|
||||
"hashMD5": "38616b85332037cc12924f2ae2840262",
|
||||
"hashSHA256": "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d",
|
||||
"tableFolders": []
|
||||
"tableFolders": [
|
||||
{
|
||||
"level": "17",
|
||||
"table": "★"
|
||||
}
|
||||
]
|
||||
},
|
||||
"tierlistInfo": {},
|
||||
"level": "?",
|
||||
"levelNum": 0,
|
||||
"difficulty": "CUSTOM",
|
||||
"difficulty": "CHART",
|
||||
"playtype": "7K",
|
||||
"isPrimary": true,
|
||||
"versions": []
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"data": {
|
||||
"subtitle": null,
|
||||
"subartist": null,
|
||||
"genre": "ELECTRANCE"
|
||||
"genre": "ELECTRANCE",
|
||||
"tableString": null
|
||||
},
|
||||
"searchTerms": [],
|
||||
"altTitles": []
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { NodeFetch } from "utils/fetch";
|
||||
* Creates a basic Fetch function used for statusCode checks.
|
||||
*/
|
||||
export function MockBasicFetch(data: Partial<Response>) {
|
||||
return (async () => data) as unknown as NodeFetch;
|
||||
return (() => data) as unknown as NodeFetch;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -15,18 +15,24 @@ export function MockBasicFetch(data: Partial<Response>) {
|
||||
* a fake JSON -> data function at that key.
|
||||
*/
|
||||
export function MockJSONFetch(urlDataMap: Record<string, unknown>) {
|
||||
return (async (url: string) => {
|
||||
if (urlDataMap[url]) {
|
||||
return {
|
||||
status: 200,
|
||||
json: async () => urlDataMap[url],
|
||||
};
|
||||
}
|
||||
return ((url: string) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (url in urlDataMap) {
|
||||
const fakeResponse = {
|
||||
status: 200,
|
||||
json: () => Promise.resolve(urlDataMap[url]),
|
||||
} as unknown as Response;
|
||||
|
||||
throw new Error(
|
||||
`Unexpected url ${url} - No Data Present? Valid urls are ${Object.keys(urlDataMap).join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
}) as NodeFetch;
|
||||
resolve(fakeResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(
|
||||
new Error(
|
||||
`Unexpected url ${url} - No Data Present? Valid urls are ${Object.keys(
|
||||
urlDataMap
|
||||
).join(", ")}`
|
||||
)
|
||||
);
|
||||
})) as NodeFetch;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export function MockMulterFile(buffer: Buffer, originalname: string) {
|
||||
const mockFile: Express.Multer.File = {
|
||||
const mockFile = {
|
||||
originalname,
|
||||
buffer,
|
||||
};
|
||||
} as unknown as Express.Multer.File;
|
||||
|
||||
return mockFile;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import { Environment, ServerConfig } from "lib/setup/config";
|
||||
import rimraf from "rimraf";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import type { StaticDatabases } from "external/mongo/db";
|
||||
import type { ICollection } from "monk";
|
||||
import type { Game } from "tachi-common";
|
||||
|
||||
// im installing an entire library for rm rf...
|
||||
|
||||
@@ -25,37 +28,37 @@ const DATA_DIR = path.join(__dirname, "./mock-db");
|
||||
const CACHE: Record<string, Array<any>> = {};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async function ResetState(data: Array<any>, collection: any) {
|
||||
async function ResetState(data: Array<any>, collection: ICollection) {
|
||||
await collection.remove({});
|
||||
|
||||
await collection.insert(data);
|
||||
}
|
||||
|
||||
function GetAndCache(filename: string, fileLoc: string) {
|
||||
let collection;
|
||||
function GetAndCache(
|
||||
filename: string,
|
||||
fileLoc: string
|
||||
): { data: Array<unknown>; collection: ICollection } {
|
||||
let collection: ICollection;
|
||||
|
||||
if (filename.startsWith("songs-")) {
|
||||
// @ts-expect-error it's right, but we know what we're doing!
|
||||
collection = db.songs[filename.split("-")[1]];
|
||||
collection = db.songs[filename.split("-")[1] as Game];
|
||||
} else if (filename.startsWith("charts-")) {
|
||||
// @ts-expect-error see above
|
||||
collection = db.charts[filename.split("-")[1]];
|
||||
collection = db.charts[filename.split("-")[1] as Game];
|
||||
} else if (filename in db) {
|
||||
collection = db[filename as StaticDatabases];
|
||||
} else {
|
||||
// @ts-expect-error see above
|
||||
collection = db[filename];
|
||||
}
|
||||
|
||||
if (!collection) {
|
||||
throw new Error(
|
||||
`Panicked when trying to get collection for ${filename}. Does this collection exist?`
|
||||
);
|
||||
}
|
||||
|
||||
if (CACHE[filename]) {
|
||||
return { data: CACHE[filename], collection };
|
||||
const cacheExists = CACHE[filename];
|
||||
|
||||
if (cacheExists) {
|
||||
return { data: cacheExists, collection };
|
||||
}
|
||||
|
||||
const data = JSON.parse(fs.readFileSync(fileLoc, "utf-8"));
|
||||
const data: unknown = JSON.parse(fs.readFileSync(fileLoc, "utf-8"));
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error(`Panic, ${filename} not JSONArray?`);
|
||||
@@ -66,7 +69,7 @@ function GetAndCache(filename: string, fileLoc: string) {
|
||||
return { data, collection };
|
||||
}
|
||||
|
||||
let CACHE_FILENAMES: Array<string>;
|
||||
let CACHE_FILENAMES: Array<string> | undefined;
|
||||
|
||||
export default async function ResetDBState() {
|
||||
let files;
|
||||
|
||||
@@ -3,4 +3,5 @@ import { monkDB } from "external/mongo/db";
|
||||
|
||||
SetIndexesForDB()
|
||||
.then(monkDB.close)
|
||||
.then(() => process.exit(0));
|
||||
.then(() => process.exit(0))
|
||||
.catch(() => process.exit(1));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { IsNonEmptyString } from "utils/misc";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
@@ -9,7 +10,7 @@ function ReadSnapshotData() {
|
||||
let snapshots: Snapshots = {};
|
||||
|
||||
if (fs.existsSync(SNAP_PATH)) {
|
||||
snapshots = JSON.parse(fs.readFileSync(SNAP_PATH, "utf-8"));
|
||||
snapshots = JSON.parse(fs.readFileSync(SNAP_PATH, "utf-8")) as Snapshots;
|
||||
}
|
||||
|
||||
return snapshots;
|
||||
@@ -22,11 +23,11 @@ export function WriteSnapshotData() {
|
||||
const snapshotData = ReadSnapshotData();
|
||||
|
||||
export function TestSnapshot(t: Tap.Test, value: string, testName: string) {
|
||||
if (process.env.TAP_SNAPSHOT) {
|
||||
if (IsNonEmptyString(process.env.TAP_SNAPSHOT)) {
|
||||
snapshotData[testName] = value;
|
||||
WriteSnapshotData();
|
||||
} else {
|
||||
if (!snapshotData[testName]) {
|
||||
if (snapshotData[testName] === undefined) {
|
||||
return t.fail(`No snapshot exists for ${testName}. Have you ran pnpm snap?`);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ const files = glob.sync(path.join(__dirname, "../../", "**/*.test.ts"));
|
||||
process.env.NODE_PATH = path.join(__dirname, "../../");
|
||||
|
||||
for (const file of files) {
|
||||
// Deliberate -- we're doing hackery here.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require(file);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ import { ApplyNTimes, RFA } from "utils/misc";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import type { DryScore } from "lib/score-import/framework/common/types";
|
||||
import type { S3Score } from "lib/score-import/import-types/file/solid-state-squad/types";
|
||||
import type { BarbatosScore } from "lib/score-import/import-types/ir/barbatos/types";
|
||||
import type { FervidexScore } from "lib/score-import/import-types/ir/fervidex/types";
|
||||
import type { KsHookSV6CScore } from "lib/score-import/import-types/ir/kshook-sv6c/types";
|
||||
import type { LR2HookScore } from "lib/score-import/import-types/ir/lr2hook/types";
|
||||
import type { USCClientScore } from "server/router/ir/usc/_playtype/types";
|
||||
@@ -33,7 +35,9 @@ import type {
|
||||
|
||||
const file = (name: string) => path.join(__dirname, "/test-data", name);
|
||||
|
||||
export const GetKTDataJSON = (name: string) => JSON.parse(fs.readFileSync(file(name), "utf-8"));
|
||||
export const GetKTDataJSON = (name: string) =>
|
||||
JSON.parse(fs.readFileSync(file(name), "utf-8")) as unknown;
|
||||
|
||||
export const GetKTDataBuffer = (name: string) => fs.readFileSync(file(name));
|
||||
|
||||
export const TestingIIDXSPDryScore: DryScore<"iidx:SP"> = {
|
||||
@@ -149,7 +153,9 @@ export const TestingSDVXScore: ScoreDocument<"sdvx:Single"> = {
|
||||
calculatedData: {},
|
||||
timeAchieved: 1619454485988,
|
||||
songID: 1,
|
||||
chartID: "5088a4d0e1ee9d0cc2f625934306e45b1a60699b", // albida adv
|
||||
|
||||
// albida adv
|
||||
chartID: "5088a4d0e1ee9d0cc2f625934306e45b1a60699b",
|
||||
highlight: false,
|
||||
isPrimary: true,
|
||||
comment: null,
|
||||
@@ -334,6 +340,20 @@ export const BMSGazerChart: ChartDocument<"bms:7K"> = {
|
||||
tierlistInfo: {},
|
||||
};
|
||||
|
||||
export const BMSGazerSong: SongDocument<"bms"> = {
|
||||
id: 27339,
|
||||
title: "gazer [MANIAQ]",
|
||||
artist: "scytheleg / obj.siokaze",
|
||||
data: {
|
||||
subtitle: null,
|
||||
subartist: null,
|
||||
genre: "ELECTRANCE",
|
||||
tableString: null,
|
||||
},
|
||||
searchTerms: [],
|
||||
altTitles: [],
|
||||
};
|
||||
|
||||
export const CHUNITHMBBKKChart: ChartDocument<"chunithm:Single"> = {
|
||||
rgcID: null,
|
||||
chartID: "192b96bdb6150f80ba6412ce02df1249e16c0cb0",
|
||||
@@ -550,7 +570,9 @@ export const TestingIIDXSPMilestoneSub: MilestoneSubscriptionDocument = {
|
||||
wasInstantlyAchieved: false,
|
||||
};
|
||||
|
||||
let KTDATA_CACHE: { songs: Array<unknown>; charts: Array<unknown> } | undefined;
|
||||
let KTDATA_CACHE:
|
||||
| { songs: Array<SongDocument<"iidx">>; charts: Array<ChartDocument<"iidx:DP" | "iidx:SP">> }
|
||||
| undefined;
|
||||
|
||||
export async function LoadTachiIIDXData() {
|
||||
let songs;
|
||||
@@ -560,8 +582,11 @@ export async function LoadTachiIIDXData() {
|
||||
songs = KTDATA_CACHE.songs;
|
||||
charts = KTDATA_CACHE.charts;
|
||||
} else {
|
||||
songs = GetKTDataJSON("./tachi/tachi-songs-iidx.json");
|
||||
charts = GetKTDataJSON("./tachi/tachi-charts-iidx.json");
|
||||
songs = GetKTDataJSON("./tachi/tachi-songs-iidx.json") as Array<SongDocument<"iidx">>;
|
||||
charts = GetKTDataJSON("./tachi/tachi-charts-iidx.json") as Array<
|
||||
ChartDocument<"iidx:DP" | "iidx:SP">
|
||||
>;
|
||||
|
||||
KTDATA_CACHE = { songs, charts };
|
||||
}
|
||||
|
||||
@@ -571,7 +596,7 @@ export async function LoadTachiIIDXData() {
|
||||
await db.charts.iidx.insert(charts);
|
||||
}
|
||||
|
||||
export const barbScore: BarbatosScore = {
|
||||
export const MockBarbatosScore: BarbatosScore = {
|
||||
clear_type: 2,
|
||||
did_fail: false,
|
||||
difficulty: 1,
|
||||
@@ -700,3 +725,280 @@ export const FakeNotification: NotificationDocument = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FervidexStaticBase = {
|
||||
name: "AIXXE",
|
||||
qpro: {
|
||||
body: 189,
|
||||
face: 138,
|
||||
hair: 76,
|
||||
hand: 145,
|
||||
head: 0,
|
||||
},
|
||||
scores: {
|
||||
"1000": {
|
||||
spa: {
|
||||
clear_type: 4,
|
||||
ex_score: 1180,
|
||||
miss_count: 40,
|
||||
},
|
||||
spn: {
|
||||
clear_type: 7,
|
||||
ex_score: 158,
|
||||
miss_count: 0,
|
||||
},
|
||||
},
|
||||
"1001": {
|
||||
dph: {
|
||||
clear_type: 3,
|
||||
ex_score: 15,
|
||||
miss_count: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
sp_dan: 15,
|
||||
};
|
||||
|
||||
export const FervidexBaseScore: FervidexScore = {
|
||||
bad: 0,
|
||||
chart: "spa",
|
||||
clear_type: 1,
|
||||
combo_break: 6,
|
||||
custom: false,
|
||||
chart_sha256: "asdfasdf",
|
||||
entry_id: 1000,
|
||||
ex_score: 68,
|
||||
fast: 0,
|
||||
gauge: [100, 50],
|
||||
ghost: [0, 2],
|
||||
good: 0,
|
||||
great: 0,
|
||||
max_combo: 34,
|
||||
option: {
|
||||
gauge: "HARD",
|
||||
range: "SUDDEN_PLUS",
|
||||
style: "RANDOM",
|
||||
},
|
||||
pacemaker: {
|
||||
name: "",
|
||||
score: 363,
|
||||
type: "PACEMAKER_A",
|
||||
},
|
||||
pgreat: 34,
|
||||
poor: 6,
|
||||
slow: 0,
|
||||
};
|
||||
|
||||
export const FervidexBaseGSMScore: FervidexScore = {
|
||||
bad: 0,
|
||||
chart: "spa",
|
||||
clear_type: 1,
|
||||
combo_break: 6,
|
||||
dead: {
|
||||
measure: 18,
|
||||
note: 40,
|
||||
},
|
||||
entry_id: 1000,
|
||||
custom: false,
|
||||
chart_sha256: "asdfasdf",
|
||||
ex_score: 68,
|
||||
fast: 0,
|
||||
gauge: [100, 50],
|
||||
ghost: [0, 2],
|
||||
good: 0,
|
||||
great: 0,
|
||||
max_combo: 34,
|
||||
option: {
|
||||
gauge: "HARD",
|
||||
range: "SUDDEN_PLUS",
|
||||
style: "RANDOM",
|
||||
},
|
||||
pacemaker: {
|
||||
name: "",
|
||||
score: 363,
|
||||
type: "PACEMAKER_A",
|
||||
},
|
||||
pgreat: 34,
|
||||
poor: 6,
|
||||
slow: 0,
|
||||
"2dx-gsm": {
|
||||
EASY: [0, 10],
|
||||
NORMAL: [0, 20],
|
||||
HARD: [20, 0],
|
||||
EX_HARD: [10, 0],
|
||||
},
|
||||
};
|
||||
|
||||
export const MockBeatorajaBMSScore = {
|
||||
chart: {
|
||||
md5: "38616b85332037cc12924f2ae2840262",
|
||||
sha256: "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d",
|
||||
title: "GAZER [MANIAQ]",
|
||||
subtitle: "",
|
||||
genre: "TRANCE",
|
||||
artist: "the dude who made gazer",
|
||||
subartist: "",
|
||||
url: "",
|
||||
appendurl: "",
|
||||
level: 8,
|
||||
total: 220,
|
||||
mode: "BEAT_7K",
|
||||
lntype: 0,
|
||||
judge: 100,
|
||||
minbpm: 135,
|
||||
maxbpm: 135,
|
||||
notes: 568,
|
||||
hasUndefinedLN: false,
|
||||
hasLN: false,
|
||||
hasCN: false,
|
||||
hasHCN: false,
|
||||
hasMine: false,
|
||||
hasRandom: false,
|
||||
hasStop: false,
|
||||
values: {},
|
||||
},
|
||||
score: {
|
||||
sha256: "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d",
|
||||
lntype: 0,
|
||||
player: "unknown",
|
||||
clear: "Easy",
|
||||
date: 0,
|
||||
epg: 332,
|
||||
lpg: 127,
|
||||
egr: 65,
|
||||
lgr: 21,
|
||||
egd: 2,
|
||||
lgd: 2,
|
||||
ebd: 1,
|
||||
lbd: 0,
|
||||
epr: 7,
|
||||
lpr: 11,
|
||||
ems: 1,
|
||||
lms: 0,
|
||||
maxcombo: 223,
|
||||
notes: 568,
|
||||
passnotes: 568,
|
||||
minbp: 20,
|
||||
option: 2,
|
||||
assist: 0,
|
||||
gauge: -1,
|
||||
deviceType: "BM_CONTROLLER",
|
||||
judgeAlgorithm: "Combo",
|
||||
rule: "LR2",
|
||||
exscore: 1004,
|
||||
},
|
||||
client: "LR2oraja 0.8.0",
|
||||
};
|
||||
|
||||
export const MockBeatorajaPMSScore = {
|
||||
chart: {
|
||||
md5: "d1253dd56bb2087d0b0d474f0d562aae",
|
||||
sha256: "a10193f7ae05ce839292dc716f182fda0b1cc6ac5382c2056f37e22ffba87b7d",
|
||||
title: "GPMSAZER [MANIAQ]",
|
||||
subtitle: "",
|
||||
genre: "Annihilate the living",
|
||||
artist: "Rocky",
|
||||
subartist: "",
|
||||
url: "",
|
||||
appendurl: "",
|
||||
level: 8,
|
||||
total: 220,
|
||||
mode: "POPN_9K",
|
||||
lntype: 0,
|
||||
judge: 100,
|
||||
minbpm: 135,
|
||||
maxbpm: 135,
|
||||
notes: 568,
|
||||
hasUndefinedLN: false,
|
||||
hasLN: false,
|
||||
hasCN: false,
|
||||
hasHCN: false,
|
||||
hasMine: false,
|
||||
hasRandom: false,
|
||||
hasStop: false,
|
||||
values: {},
|
||||
},
|
||||
score: {
|
||||
sha256: "a10193f7ae05ce839292dc716f182fda0b1cc6ac5382c2056f37e22ffba87b7d",
|
||||
lntype: 0,
|
||||
player: "unknown",
|
||||
clear: "Easy",
|
||||
date: 0,
|
||||
epg: 332,
|
||||
lpg: 127,
|
||||
egr: 65,
|
||||
lgr: 21,
|
||||
egd: 2,
|
||||
lgd: 2,
|
||||
ebd: 1,
|
||||
lbd: 0,
|
||||
epr: 7,
|
||||
lpr: 11,
|
||||
ems: 1,
|
||||
lms: 0,
|
||||
maxcombo: 223,
|
||||
notes: 568,
|
||||
passnotes: 568,
|
||||
minbp: 20,
|
||||
option: 2,
|
||||
assist: 0,
|
||||
gauge: -1,
|
||||
deviceType: "BM_CONTROLLER",
|
||||
judgeAlgorithm: "Combo",
|
||||
rule: "LR2",
|
||||
exscore: 1004,
|
||||
},
|
||||
client: "beatoraja 0.8.0",
|
||||
};
|
||||
|
||||
export const MockParsedS3Score: S3Score = {
|
||||
id: 187,
|
||||
diff: "A",
|
||||
songname: "5.1.1.",
|
||||
styles: "7th",
|
||||
exscore: 100,
|
||||
scorebreakdown: {
|
||||
justgreats: 25,
|
||||
greats: 50,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
poor: 4,
|
||||
},
|
||||
mods: {},
|
||||
cleartype: "perfect",
|
||||
date: "2010-10-19 04:54:22",
|
||||
};
|
||||
|
||||
export const FakeSmallBatchManual = {
|
||||
meta: {
|
||||
game: "iidx",
|
||||
playtype: "SP",
|
||||
service: "foobar",
|
||||
},
|
||||
scores: [
|
||||
{
|
||||
score: 500,
|
||||
lamp: "HARD CLEAR",
|
||||
matchType: "songTitle",
|
||||
identifier: "5.1.1.",
|
||||
difficulty: "ANOTHER",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const FakeChunitachiBatchManual = {
|
||||
meta: {
|
||||
game: "chunithm",
|
||||
playtype: "Single",
|
||||
service: "ChunItachi",
|
||||
},
|
||||
scores: [
|
||||
{
|
||||
score: 900000,
|
||||
lamp: "CLEAR",
|
||||
matchType: "songTitle",
|
||||
identifier: "B.B.K.K.B.K.K.",
|
||||
difficulty: "BASIC",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"difficulty": 1,
|
||||
"level": 10,
|
||||
"song_id": 1,
|
||||
"max_chain": 50,
|
||||
"critical": 50,
|
||||
"near_total": 30,
|
||||
"near_fast": 20,
|
||||
"near_slow": 10,
|
||||
"score": 9500000,
|
||||
"error": 5,
|
||||
"percent": 81,
|
||||
"did_fail": false,
|
||||
"clear_type": 2,
|
||||
"gauge_type": 0,
|
||||
"is_skill_analyzer": false
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"chart": {
|
||||
"md5": "38616b85332037cc12924f2ae2840262",
|
||||
"sha256": "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d",
|
||||
"title": "GAZER [MANIAQ]",
|
||||
"subtitle": "",
|
||||
"genre": "TRANCE",
|
||||
"artist": "the dude who made gazer",
|
||||
"subartist": "",
|
||||
"url": "",
|
||||
"appendurl": "",
|
||||
"level": 8,
|
||||
"total": 220,
|
||||
"mode": "BEAT_7K",
|
||||
"lntype": 0,
|
||||
"judge": 100,
|
||||
"minbpm": 135,
|
||||
"maxbpm": 135,
|
||||
"notes": 568,
|
||||
"hasUndefinedLN": false,
|
||||
"hasLN": false,
|
||||
"hasCN": false,
|
||||
"hasHCN": false,
|
||||
"hasMine": false,
|
||||
"hasRandom": false,
|
||||
"hasStop": false,
|
||||
"values": {}
|
||||
},
|
||||
"score": {
|
||||
"sha256": "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d",
|
||||
"lntype": 0,
|
||||
"player": "unknown",
|
||||
"clear": "Easy",
|
||||
"date": 0,
|
||||
"epg": 332,
|
||||
"lpg": 127,
|
||||
"egr": 65,
|
||||
"lgr": 21,
|
||||
"egd": 2,
|
||||
"lgd": 2,
|
||||
"ebd": 1,
|
||||
"lbd": 0,
|
||||
"epr": 7,
|
||||
"lpr": 11,
|
||||
"ems": 1,
|
||||
"lms": 0,
|
||||
"maxcombo": 223,
|
||||
"notes": 568,
|
||||
"passnotes": 568,
|
||||
"minbp": 20,
|
||||
"option": 2,
|
||||
"assist": 0,
|
||||
"gauge": -1,
|
||||
"deviceType": "BM_CONTROLLER",
|
||||
"judgeAlgorithm": "Combo",
|
||||
"rule": "LR2",
|
||||
"exscore": 1004
|
||||
},
|
||||
"client": "LR2oraja 0.8.0"
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"chart": {
|
||||
"md5": "d1253dd56bb2087d0b0d474f0d562aae",
|
||||
"sha256": "a10193f7ae05ce839292dc716f182fda0b1cc6ac5382c2056f37e22ffba87b7d",
|
||||
"title": "GPMSAZER [MANIAQ]",
|
||||
"subtitle": "",
|
||||
"genre": "Annihilate the living",
|
||||
"artist": "Rocky",
|
||||
"subartist": "",
|
||||
"url": "",
|
||||
"appendurl": "",
|
||||
"level": 8,
|
||||
"total": 220,
|
||||
"mode": "POPN_9K",
|
||||
"lntype": 0,
|
||||
"judge": 100,
|
||||
"minbpm": 135,
|
||||
"maxbpm": 135,
|
||||
"notes": 568,
|
||||
"hasUndefinedLN": false,
|
||||
"hasLN": false,
|
||||
"hasCN": false,
|
||||
"hasHCN": false,
|
||||
"hasMine": false,
|
||||
"hasRandom": false,
|
||||
"hasStop": false,
|
||||
"values": {}
|
||||
},
|
||||
"score": {
|
||||
"sha256": "a10193f7ae05ce839292dc716f182fda0b1cc6ac5382c2056f37e22ffba87b7d",
|
||||
"lntype": 0,
|
||||
"player": "unknown",
|
||||
"clear": "Easy",
|
||||
"date": 0,
|
||||
"epg": 332,
|
||||
"lpg": 127,
|
||||
"egr": 65,
|
||||
"lgr": 21,
|
||||
"egd": 2,
|
||||
"lgd": 2,
|
||||
"ebd": 1,
|
||||
"lbd": 0,
|
||||
"epr": 7,
|
||||
"lpr": 11,
|
||||
"ems": 1,
|
||||
"lms": 0,
|
||||
"maxcombo": 223,
|
||||
"notes": 568,
|
||||
"passnotes": 568,
|
||||
"minbp": 20,
|
||||
"option": 2,
|
||||
"assist": 0,
|
||||
"gauge": -1,
|
||||
"deviceType": "BM_CONTROLLER",
|
||||
"judgeAlgorithm": "Combo",
|
||||
"rule": "LR2",
|
||||
"exscore": 1004
|
||||
},
|
||||
"client": "beatoraja 0.8.0"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"title": "ALBIDA Powerless Mix",
|
||||
"difficulty": "ADVANCED",
|
||||
"level": "10",
|
||||
"lamp": "EXCESSIVE COMPLETE",
|
||||
"score": "9310699",
|
||||
"exscore": "0"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "AIXXE",
|
||||
"qpro": {
|
||||
"body": 189,
|
||||
"face": 138,
|
||||
"hair": 76,
|
||||
"hand": 145,
|
||||
"head": 0
|
||||
},
|
||||
"scores": {
|
||||
"1000": {
|
||||
"spa": {
|
||||
"clear_type": 4,
|
||||
"ex_score": 1180,
|
||||
"miss_count": 40
|
||||
},
|
||||
"spn": {
|
||||
"clear_type": 7,
|
||||
"ex_score": 158,
|
||||
"miss_count": 0
|
||||
}
|
||||
},
|
||||
"1001": {
|
||||
"dph": {
|
||||
"clear_type": 3,
|
||||
"ex_score": 15,
|
||||
"miss_count": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"sp_dan": 15
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{
|
||||
"bad": 0,
|
||||
"chart": "spa",
|
||||
"clear_type": 1,
|
||||
"combo_break": 6,
|
||||
"dead": {
|
||||
"measure": 18,
|
||||
"note": 40
|
||||
},
|
||||
"entry_id": 1000,
|
||||
"custom": false,
|
||||
"chart_sha256": "asdfasdf",
|
||||
"ex_score": 68,
|
||||
"fast": 0,
|
||||
"gauge": [
|
||||
100,
|
||||
50
|
||||
],
|
||||
"ghost": [
|
||||
0,
|
||||
2
|
||||
],
|
||||
"good": 0,
|
||||
"great": 0,
|
||||
"max_combo": 34,
|
||||
"option": {
|
||||
"gauge": "HARD",
|
||||
"range": "SUDDEN_PLUS",
|
||||
"style": "RANDOM"
|
||||
},
|
||||
"pacemaker": {
|
||||
"name": "",
|
||||
"score": 363,
|
||||
"type": "PACEMAKER_A"
|
||||
},
|
||||
"pgreat": 34,
|
||||
"poor": 6,
|
||||
"slow": 0,
|
||||
"2dx-gsm": {
|
||||
"EASY": [
|
||||
0,
|
||||
10
|
||||
],
|
||||
"NORMAL": [
|
||||
0,
|
||||
20
|
||||
],
|
||||
"HARD": [
|
||||
20,
|
||||
0
|
||||
],
|
||||
"EX_HARD": [
|
||||
10,
|
||||
0
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"bad": 0,
|
||||
"chart": "spa",
|
||||
"clear_type": 1,
|
||||
"combo_break": 6,
|
||||
"custom": false,
|
||||
"chart_sha256": "asdfasdf",
|
||||
"entry_id": 1000,
|
||||
"ex_score": 68,
|
||||
"fast": 0,
|
||||
"gauge": [
|
||||
100,
|
||||
50
|
||||
],
|
||||
"ghost": [
|
||||
0,
|
||||
2
|
||||
],
|
||||
"good": 0,
|
||||
"great": 0,
|
||||
"max_combo": 34,
|
||||
"option": {
|
||||
"gauge": "HARD",
|
||||
"range": "SUDDEN_PLUS",
|
||||
"style": "RANDOM"
|
||||
},
|
||||
"pacemaker": {
|
||||
"name": "",
|
||||
"score": 363,
|
||||
"type": "PACEMAKER_A"
|
||||
},
|
||||
"pgreat": 34,
|
||||
"poor": 6,
|
||||
"slow": 0
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"music_id": 1000,
|
||||
"play_type": "SINGLE",
|
||||
"diff_type": "ANOTHER",
|
||||
"score": 1000,
|
||||
"miss_count": 21,
|
||||
"clear_type": "CLEAR",
|
||||
"update_time": "2021-03-24 07:15:22"
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"id": 187,
|
||||
"diff": "A",
|
||||
"songname": "5.1.1.",
|
||||
"styles": "7th",
|
||||
"exscore": 100,
|
||||
"scorebreakdown": {
|
||||
"justgreats": 25,
|
||||
"greats": 50,
|
||||
"good": 0,
|
||||
"bad": 0,
|
||||
"poor": 4
|
||||
},
|
||||
"mods": {},
|
||||
"cleartype": "perfect",
|
||||
"date": "2010-10-19 04:54:22"
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"songID": 27339,
|
||||
"chartID": "88eb6cc5683e2740cbd07f588a5f3db1db8d467b",
|
||||
"rgcID": null,
|
||||
"data": {
|
||||
"notecount": 2256,
|
||||
"hashMD5": "38616b85332037cc12924f2ae2840262",
|
||||
"hashSHA256": "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d"
|
||||
},
|
||||
"level": "?",
|
||||
"levelNum": 0,
|
||||
"difficulty": "CUSTOM",
|
||||
"playtype": "7K",
|
||||
"isPrimary": true,
|
||||
"versions": []
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"id": 27339,
|
||||
"title": "gazer [MANIAQ]",
|
||||
"artist": "scytheleg / obj.siokaze",
|
||||
"data": {
|
||||
"subtitle": null,
|
||||
"subartist": null,
|
||||
"genre": "ELECTRANCE"
|
||||
},
|
||||
"searchTerms": [],
|
||||
"altTitles": []
|
||||
}
|
||||
@@ -24,8 +24,8 @@ export async function RecalcSessions(filter = {}) {
|
||||
try {
|
||||
c = CreateSessionCalcData(session.game, session.playtype, scores);
|
||||
} catch (err) {
|
||||
logger.error(`${session.game} (${session.playtype}) failed.`);
|
||||
logger.warn(`Destroying session.`);
|
||||
logger.error(`Recalcing ${session.game} (${session.playtype}) failed.`, { err });
|
||||
logger.warn(`Destroying session!`);
|
||||
await db.sessions.remove({ sessionID: session.sessionID });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ export async function EfficientDBIterate<T, R>(
|
||||
) {
|
||||
let i = 0;
|
||||
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
logger.info(`Running on ${i} - ${i + bucketSize} documents.`);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
|
||||
+53
-36
@@ -40,39 +40,54 @@ export async function ResolveFolderToCharts(
|
||||
let songs: Array<SongDocument> | null = null;
|
||||
let charts: Array<ChartDocument>;
|
||||
|
||||
if (folder.type === "static") {
|
||||
charts = await db.charts[folder.game].find(
|
||||
deepmerge(filter, {
|
||||
playtype: folder.playtype, // mandatory
|
||||
chartID: { $in: folder.data },
|
||||
})
|
||||
);
|
||||
} else if (folder.type === "songs") {
|
||||
songs = await db.songs[folder.game].find(folder.data);
|
||||
switch (folder.type) {
|
||||
case "static": {
|
||||
charts = await db.charts[folder.game].find(
|
||||
deepmerge(filter, {
|
||||
// Specifying playtype is mandatory, don't want to catch other charts.
|
||||
playtype: folder.playtype,
|
||||
chartID: { $in: folder.data },
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
charts = await db.charts[folder.game].find(
|
||||
deepmerge(filter, {
|
||||
playtype: folder.playtype,
|
||||
songID: { $in: songs.map((e) => e.id) },
|
||||
})
|
||||
);
|
||||
} else if (folder.type === "charts") {
|
||||
const folderDataTransposed = TransposeFolderData(folder.data);
|
||||
case "songs": {
|
||||
songs = await db.songs[folder.game].find(folder.data);
|
||||
|
||||
logger.debug(`Transposed folder data in resolve-folder-to-charts.`, {
|
||||
folder,
|
||||
folderDataTransposed,
|
||||
});
|
||||
charts = await db.charts[folder.game].find(
|
||||
deepmerge(filter, {
|
||||
playtype: folder.playtype,
|
||||
songID: { $in: songs.map((e) => e.id) },
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const fx = deepmerge.all([filter, { playtype: folder.playtype }, folderDataTransposed]);
|
||||
case "charts": {
|
||||
const folderDataTransposed = TransposeFolderData(folder.data);
|
||||
|
||||
charts = await db.charts[folder.game].find(fx);
|
||||
} else {
|
||||
// @ts-expect-error This is already a weird scenario. Shouldn't fail, though.
|
||||
logger.error(`Invalid folder at ${folder.folderID}. Cannot resolve.`, { folder });
|
||||
logger.debug(`Transposed folder data in resolve-folder-to-charts.`, {
|
||||
folder,
|
||||
folderDataTransposed,
|
||||
});
|
||||
|
||||
// @ts-expect-error See above
|
||||
throw new Error(`Invalid folder ${folder.folderID}. Cannot resolve.`);
|
||||
const fx = deepmerge.all([filter, { playtype: folder.playtype }, folderDataTransposed]);
|
||||
|
||||
charts = await db.charts[folder.game].find(fx);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
logger.error(
|
||||
`Invalid folder at ${(folder as FolderDocument).folderID}. Cannot resolve.`,
|
||||
{ folder }
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`Invalid folder ${(folder as FolderDocument).folderID}. Cannot resolve.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (getSongs) {
|
||||
@@ -98,10 +113,14 @@ export async function ResolveFolderToCharts(
|
||||
export function TransposeFolderData(obj: Record<string, unknown>) {
|
||||
const transposedObj: Record<string, unknown> = {};
|
||||
|
||||
for (const key in obj) {
|
||||
for (const key of Object.keys(obj)) {
|
||||
const transposedKey = key.replace(/~/gu, "$").replace(/¬/gu, ".");
|
||||
|
||||
if (typeof obj[key] === "object" && !Array.isArray(obj[key]) && obj[key]) {
|
||||
if (
|
||||
typeof obj[key] === "object" &&
|
||||
!Array.isArray(obj[key]) &&
|
||||
(obj[key] as object | null)
|
||||
) {
|
||||
transposedObj[transposedKey] = TransposeFolderData(obj[key] as Record<string, unknown>);
|
||||
} else {
|
||||
transposedObj[transposedKey] = obj[key];
|
||||
@@ -236,9 +255,8 @@ export function CalculateLampDistribution(pbs: Array<PBScoreDocument>) {
|
||||
const lampDist: Partial<Record<Lamps[IDStrings], integer>> = {};
|
||||
|
||||
for (const pb of pbs) {
|
||||
if (lampDist[pb.scoreData.lamp]) {
|
||||
// @ts-expect-error ???
|
||||
lampDist[pb.scoreData.lamp]++;
|
||||
if (lampDist[pb.scoreData.lamp] !== undefined) {
|
||||
lampDist[pb.scoreData.lamp]!++;
|
||||
} else {
|
||||
lampDist[pb.scoreData.lamp] = 1;
|
||||
}
|
||||
@@ -251,9 +269,8 @@ export function CalculateGradeDistribution(pbs: Array<PBScoreDocument>) {
|
||||
const gradeDist: Partial<Record<Grades[IDStrings], integer>> = {};
|
||||
|
||||
for (const pb of pbs) {
|
||||
if (gradeDist[pb.scoreData.grade]) {
|
||||
// @ts-expect-error ???
|
||||
gradeDist[pb.scoreData.grade]++;
|
||||
if (gradeDist[pb.scoreData.grade] !== undefined) {
|
||||
gradeDist[pb.scoreData.grade]!++;
|
||||
} else {
|
||||
gradeDist[pb.scoreData.grade] = 1;
|
||||
}
|
||||
|
||||
@@ -255,3 +255,7 @@ export function NotNullish<T>(maybeValue: T | null | undefined): T {
|
||||
export function IsNullish<T>(maybeValue: T | null | undefined): maybeValue is null | undefined {
|
||||
return maybeValue === null || maybeValue === undefined;
|
||||
}
|
||||
|
||||
export function IsRecord(maybeRecord: unknown): maybeRecord is Record<string, unknown> {
|
||||
return typeof maybeRecord === "object" && maybeRecord !== null;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export function NaiveCSVParse(csvBuffer: Buffer, logger: KtLogger) {
|
||||
|
||||
const rawRows = [];
|
||||
|
||||
for (const [rowNumber, data] of Object.entries(csvData)) {
|
||||
for (const [rowNumber, data] of Object.entries(csvData).slice(1)) {
|
||||
// @security: This should probably be safetied from DOSing
|
||||
const cells = data.split(",");
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ export function FormatPrError(err: PrudenceError, foreword = "Error") {
|
||||
const receivedText =
|
||||
typeof err.userVal === "object" && err.userVal !== null
|
||||
? ""
|
||||
: ` | Received ${err.userVal} [${err.userVal === null ? "null" : typeof err.userVal}]`;
|
||||
: ` | Received ${String(err.userVal)} [${
|
||||
err.userVal === null ? "null" : typeof err.userVal
|
||||
}]`;
|
||||
|
||||
return `${foreword}: ${err.keychain ?? "null"} | ${err.message}${receivedText}.`;
|
||||
}
|
||||
|
||||
@@ -297,30 +297,32 @@ export async function FindChartsOnPopularity(
|
||||
}
|
||||
>;
|
||||
|
||||
const scoreCounts = await db[scoreCollection].aggregate([
|
||||
{
|
||||
$match: { chartID: { $in: charts.map((e) => e.chartID) } },
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: "$chartID",
|
||||
count: { $sum: 1 },
|
||||
const scoreCounts: Array<{ _id: string; count: integer }> = await db[scoreCollection].aggregate(
|
||||
[
|
||||
{
|
||||
$match: { chartID: { $in: charts.map((e) => e.chartID) } },
|
||||
},
|
||||
},
|
||||
{
|
||||
$sort: {
|
||||
count: -1,
|
||||
{
|
||||
$group: {
|
||||
_id: "$chartID",
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$skip: skip,
|
||||
},
|
||||
{
|
||||
$limit: limit,
|
||||
},
|
||||
]);
|
||||
{
|
||||
$sort: {
|
||||
count: -1,
|
||||
},
|
||||
},
|
||||
{
|
||||
$skip: skip,
|
||||
},
|
||||
{
|
||||
$limit: limit,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
const scoreCountMap = new Map();
|
||||
const scoreCountMap = new Map<string, integer>();
|
||||
|
||||
for (const sc of scoreCounts) {
|
||||
scoreCountMap.set(sc._id, sc.count);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import db from "external/mongo/db";
|
||||
import type { Condition } from "mongodb";
|
||||
import type { ScoreDocument, SessionDocument, SessionScoreInfo } from "tachi-common";
|
||||
import type { ScoreDocument, SessionDocument } from "tachi-common";
|
||||
|
||||
/**
|
||||
* Returns all the score documents inside a session.
|
||||
@@ -18,7 +17,6 @@ export function GetScoresFromSession(session: SessionDocument) {
|
||||
*/
|
||||
export function GetSessionFromScore(score: ScoreDocument) {
|
||||
return db.sessions.findOne({
|
||||
// ??? another bug in monks types i think
|
||||
scoreInfo: { scoreID: score.scoreID } as Condition<SessionScoreInfo>,
|
||||
"scoreInfo.scoreID": score.scoreID,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,6 +27,19 @@ declare module "express-session" {
|
||||
}
|
||||
}
|
||||
|
||||
declare module "express-serve-static-core" {
|
||||
export interface Request {
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// KNOWN BUG IN TS-ESLINT.
|
||||
/**
|
||||
* This is a type-safe variant of "req.safeBody".
|
||||
* "req.safeBody" is 'any' by default, which makes it exceptionally difficult
|
||||
* to use in our codebase (due to the strict hollis rules.)
|
||||
*/
|
||||
safeBody: Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
||||
export interface TachiSessionData {
|
||||
user: PublicUserDocument;
|
||||
settings: UserSettings;
|
||||
|
||||
@@ -142,7 +142,7 @@ export function FormatUserDoc(userdoc: PublicUserDocument) {
|
||||
export async function GetUsersRanking(stats: UserGameStats) {
|
||||
const gptConfig = GetGamePTConfig(stats.game, stats.playtype);
|
||||
|
||||
const aggRes = await db["game-stats"].aggregate([
|
||||
const aggRes: [{ _id: null; ranking: integer }] = await db["game-stats"].aggregate([
|
||||
{
|
||||
$match: {
|
||||
game: stats.game,
|
||||
@@ -170,7 +170,7 @@ export async function GetUsersRanking(stats: UserGameStats) {
|
||||
},
|
||||
]);
|
||||
|
||||
return (aggRes[0].ranking + 1) as integer;
|
||||
return aggRes[0].ranking + 1;
|
||||
}
|
||||
|
||||
export function GetUGPTPlaycount(userID: integer, game: Game, playtype: Playtype) {
|
||||
@@ -257,7 +257,7 @@ export async function IsRequesterAdmin(request: APITokenDocument) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!request.userID) {
|
||||
if (request.userID === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user