ai bug audit 1 (#1545)

* fix: use highlights in tachi rival table

* fix: silentReject forgot pw

* fix: bms table sync typo

* fix: typo

* fix: mass typos

* fix: tests
This commit is contained in:
zk
2026-05-21 19:13:23 +01:00
committed by GitHub
parent d475da367d
commit 05bc8733df
44 changed files with 77 additions and 83 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# Admin Endpoints
These endpoints are for adminstrator use. As such, they all
require an `authLevel` of atleast 3. For more information, see the [UserDocument](../../schemas/user.md).
require an `authLevel` of at least 3. For more information, see the [UserDocument](../../schemas/user.md).
*****
+1 -1
View File
@@ -78,7 +78,7 @@ The properties are described as this:
| `artist` (Conditional) | String | If `matchType` is "songTitle", this field can be present, and describe the artist name for less equivocal matching. This field is optional for legacy purposes. |
| `timeAchieved` (Optional) | integer \| null | This is *when* the score was achieved in unix milliseconds. This should be provided if possible, as Tachi uses it for a LOT of features. |
| `comment` (Optional) | string \| null | A comment from the user about this score. |
| `judgements` (Optional) | Record<Game Judgement, integer> | This should be a record of the judgements for your game + playtype, and the integer indicating how often they occured. |
| `judgements` (Optional) | Record<Game Judgement, integer> | This should be a record of the judgements for your game + playtype, and the integer indicating how often they occurred. |
| `optional` (Optional) | See [Game Specific Optional Metrics](../../schemas/score.md#game-specific) | Any optional metrics you wish to provide for this game. |
| `scoreMeta` (Optional) | See [Game Specific Score Meta](../../schemas/score.md#game-specific) | This can be a partial record of various `scoreMeta` props for this game. |
+1 -1
View File
@@ -315,7 +315,7 @@ The song this score is for.
Converters are also expected to fail.
For this, we have a very specific throw format
that should be thrown whenever a specific error has occured.
that should be thrown whenever a specific error has occurred.
These are called Failures, and a full list of them can be
found [here](./conv-failures.md).
@@ -172,7 +172,7 @@ public class TachiIR implements IRConnection {
* Utility wrapper for throwing a generic exception.
*/
private void _throw() throws Exception {
throw new Exception("An internal error has occured.");
throw new Exception("An internal error has occurred.");
}
/**
@@ -264,7 +264,7 @@ public class TachiIR implements IRConnection {
_throw();
}
} else {
log("An error has occured in logging in. Please make sure that you are putting an API Key in your password field, and not your site login password.",
log("An error has occurred in logging in. Please make sure that you are putting an API Key in your password field, and not your site login password.",
Importance.ERROR);
_throw();
}
@@ -419,7 +419,7 @@ public class TachiIR implements IRConnection {
return rc.create(resp.success, resp.description, irScoreArr);
} catch (Exception e) {
log("An error has occured while fetching scores for " + model.title + " (" + model.sha256 + ")",
log("An error has occurred while fetching scores for " + model.title + " (" + model.sha256 + ")",
Importance.ERROR);
e.printStackTrace(System.out);
return rc.create(false, "Internal Exception", null);
@@ -60,10 +60,10 @@ export async function handleIsCommand(
});
}
} catch (err) {
log.error({ command, err }, `An error occured while executing a command.`);
log.error({ command, err }, `An error occurred while executing a command.`);
void interaction.editReply(
`An error has occured while executing this command (${err}). This has been reported.`,
`An error has occurred while executing this command (${err}). This has been reported.`,
);
}
} catch (e) {
+2 -2
View File
@@ -91,7 +91,7 @@ app.get("/oauth/callback", async (req, res) => {
return res
.status(500)
.send(
"Something's gone very wrong. An internal server error has occured. This has been reported.",
"Something's gone very wrong. An internal server error has occurred. This has been reported.",
);
}
@@ -207,7 +207,7 @@ const MainExpressErrorHandler: express.ErrorRequestHandler = (err, req, res, _ne
return res.status(500).json({
success: false,
description: "A fatal internal server error has occured.",
description: "A fatal internal server error has occurred.",
});
};
@@ -102,7 +102,7 @@ For now, all quests are built-in to Tachi. Ideally, we want the built-in Tachi q
You shouldn't make goals like playing everything in a large folder, since those are more tests of endurance than skill. They're just going to be wasting their time!
You should also avoid redundant goals -- all the goals in the quest should ideally be around the same difficulty! Something like Clear 10 18s and Clear 25 18s are obviously redundant, but this also applies to goals like AAA Easy_Chart and Way_harder_chart. Every goal and part of a goal in a quest should be something a player can reasonably have last to achieve.`,
builtin_questline: `I want every game on Tachi to have atleast one fairly comprehensive questline (see \`/faq whatare_questlines\`). These should be useful for players of all skill levels, so that they have some good ideas for goals to set around their level.
builtin_questline: `I want every game on Tachi to have at least one fairly comprehensive questline (see \`/faq whatare_questlines\`). These should be useful for players of all skill levels, so that they have some good ideas for goals to set around their level.
I'm ideally looking for something that the average player can just set and have fun checking off. The questline itself should have the quests fairly distinguished, players shouldn't be pushing multiple quests in the same questline at the same time.
@@ -24,7 +24,7 @@ export default function RegisterPage() {
const [err, setErr] = useState("");
// not opened: user has not clicked the rules link
// opened: user has clicked the rules link
// read: atleast 30 seconds have expired.
// read: at least 30 seconds have expired.
const [readRules, setReadRules] = useState<"acknowledged" | "not-opened" | "opened" | "read">(
"not-opened",
);
@@ -117,7 +117,7 @@ export default function BeatorajaIRPage({ game }: { game: "bms" | "pms" }) {
<Divider />
<Muted>
Note: If you submit a score on a chart that {TachiConfig.NAME} doesn't recognise,
you'll need to wait until atleast 2 other players submit scores for that chart
you'll need to wait until at least 2 other players submit scores for that chart
before it'll show up. This is to combat accidental IR spam.
</Muted>
</div>
@@ -30,7 +30,7 @@ export default function TimelineChart({
Not Enough Data... Yet.
<br />
<small className="text-body-secondary">
(You need atleast 2 days worth of data)
(You need at least 2 days worth of data)
</small>
</div>
</div>
@@ -4,7 +4,7 @@ export default function NoDataWrapper({ children }: { children: JSX.Element[] })
if (children.length === 0) {
return (
<tr>
{/* Slight hack - span atleast 100 columns, which is probably what we want. */}
{/* Slight hack - span at least 100 columns, which is probably what we want. */}
<td colSpan={100}>
<span className="text-center">No Data.</span>
</td>
+2 -2
View File
@@ -61,7 +61,7 @@ export interface GoalSingleCriteria<TGame extends V3Game = V3Game> extends GoalC
/**
* Criteria for a score to match this criteria - this is a "count" mode, which means that
* atleast N scores have to match this criteria. This is for things like folders.
* at least N scores have to match this criteria. This is for things like folders.
*/
export interface GoalCountCriteria<TGame extends V3Game = V3Game> extends GoalCriteria<TGame> {
mode: "absolute" | "proportion";
@@ -470,7 +470,7 @@ export interface PBReference {
}
export interface PBScoreDocument<TGame extends V3Game = V3Game> {
// guaranteed to atleast have one element.
// guaranteed to at least have one element.
composedFrom: [PBReference, ...Array<PBReference>];
rankingData: {
outOf: integer;
+1 -1
View File
@@ -205,7 +205,7 @@ const react = /** @type {import("@typescript-eslint/utils/ts-eslint").FlatConfig
"react/jsx-uses-react": ["off"],
"react-hooks/rules-of-hooks": ["error"],
// Knowing when you're inexhaustive is very valuable, but actually being
// exhaustive is not always what you want, infact, we frequently don't want it.
// exhaustive is not always what you want, in fact, we frequently don't want it.
"react-hooks/exhaustive-deps": ["warn"],
},
settings: {
@@ -1,6 +1,7 @@
/* eslint-disable no-await-in-loop */
import { MakeAction } from "#lib/actions/actions";
import { rebuildFolderChartLookup } from "#lib/folders/rebuild-folder-chart-lookup";
import { log } from "#lib/log/log";
import { DeorphanBmsIfInOrphanChartPg } from "#lib/orphan-queue/deorphan-bms-pg";
import DB from "#services/pg/db";
@@ -34,7 +35,7 @@ function stripTableFoldersKeySql(prefix: string) {
/**
* Tables might have updates that remove charts from their table.
*
* We need to handle this -- infact, it's quite common for something
* We need to handle this -- in fact, it's quite common for something
* to go from the sl12 folder to st0 -- which is a cross-table
* change.
*/
@@ -153,7 +154,7 @@ async function ImportTableLevels(
if (!chart) {
log.warn(
`No chart exists in table for (${td.checksum.type}=${td.checksum.value} Possible title: ${td.content.title} ${prefix}${td.content.level}`,
`No chart exists in table for (${td.checksum.type}=${td.checksum.value})w Possible title: ${td.content.title} ${prefix}${td.content.level}`,
);
failures++;
continue;
@@ -225,7 +226,7 @@ export async function syncBmsTablesCore() {
}
log.info(`Re-initialising folder-chart-lookup, since changes may have been made.`);
// await InitaliseFolderChartLookup();
await rebuildFolderChartLookup(DB);
log.info(`Done.`);
}
@@ -123,32 +123,25 @@ describe("ANON_ACTION_ForgotPassword", () => {
// ── Email not found ────────────────────────────────────────────────────────
it("throws when no account is registered with the given email", async () => {
await expect(
ANON_ACTION_ForgotPassword(taker, { "!email": "nobody@example.com" }),
).rejects.toThrow();
it("returns { silentlyRejected: true } when no account is registered with the given email", async () => {
const result = await ANON_ACTION_ForgotPassword(taker, { "!email": "nobody@example.com" });
expect(result).toEqual({ silentlyRejected: true });
});
it("writes a THROW action row when the email is not found (dead executeTakeFirstOrThrow)", async () => {
await expect(
ANON_ACTION_ForgotPassword(taker, { "!email": "nobody@example.com" }),
).rejects.toThrow();
it("writes a GOOD action row when the email is not found", async () => {
await ANON_ACTION_ForgotPassword(taker, { "!email": "nobody@example.com" });
const action = await DB.selectFrom("action")
.select("result")
.where("kind", "=", "FORGOT_PASSWORD")
.executeTakeFirstOrThrow();
// executeTakeFirstOrThrow raises a raw NoResultError (not an ExpectedErr),
// so the audit framework marks it THROW rather than BAD.
// Note: the silentlyRejected branch in the source is dead code.
expect(action.result).toBe("THROW");
expect(action.result).toBe("GOOD");
});
it("does not insert any token when the email is not found", async () => {
await expect(
ANON_ACTION_ForgotPassword(taker, { "!email": "nobody@example.com" }),
).rejects.toThrow();
await ANON_ACTION_ForgotPassword(taker, { "!email": "nobody@example.com" });
const rows = await DB.selectFrom("priv_password_reset_token")
.select("token")
@@ -18,7 +18,7 @@ export const ANON_ACTION_ForgotPassword = MakeAnonAction(
const userPrivateInfo = await DB.selectFrom("priv_account_credential")
.select(["user_id", "email"])
.where("email", "=", email)
.executeTakeFirstOrThrow();
.executeTakeFirst();
if (userPrivateInfo) {
const user = await GetUserWithIDGuaranteed(userPrivateInfo.user_id);
@@ -137,7 +137,7 @@ export type PBRankingValuesFunction<TGame extends V3Game> = (
/**
* Format a goal into a string. If a function is provided, it's called with this goals
* criteria value, so a goal of "get 3600 on $CHART" would recieve 3600 as its
* criteria value, so a goal of "get 3600 on $CHART" would receive 3600 as its
* argument.
*/
export type GoalCriteriaFormatter = (num: number) => string;
@@ -102,7 +102,7 @@ function whereMsRangeOnColumn(
* Optionally, startFrom can be passed, which will start this activity search from that
* point in time.
*
* @bug - With the way `startFrom` works, its possible to "skip over" sessions that have
* @bug - With the way `startFrom` works, it's possible to "skip over" sessions that have
* the **exact** same timestamp, but didn't fall into the previous limit.
*
* for an array of imagined timestamps with sessions=3, followed by startFrom=3
@@ -71,7 +71,7 @@ const KamaiDefaultClients: DefaultClients = [
webhookUri: null,
apiKeyFilename: "ChunItachi.ini",
apiKeyTemplate: `[general]
; Show information for debugging. Useful incase things go wrong.
; Show information for debugging. Useful in case things go wrong.
showDebug = true
; Set this to an extID if multiple people play on your setup
@@ -10,7 +10,7 @@ import {
GetFoldersFromTable,
GetTableForIDGuaranteed,
} from "#utils/folder";
import { GetRecentUGPTScores } from "#utils/queries/scores";
import { GetRecentUGPTHighlights, GetRecentUGPTScores } from "#utils/queries/scores";
import { REQ_GetGame, REQ_GetUser } from "#utils/req-tachi-data";
import { type BMSTableHead, LoadBMSTable, type RawBMSTableEntry } from "bms-table-loader";
import path from "path";
@@ -112,7 +112,7 @@ export type TachiBMSTable = {
game: GamesForGroup["bms"],
) => Promise<Array<RawBMSTableEntry>>;
// like, say, their rivals scores or something.
// then the callbacks need to recieve that info.
// then the callbacks need to receive that info.
getLevelOrder: (
userID: integer,
game: GamesForGroup["bms"],
@@ -163,7 +163,7 @@ function GetUserID(req: Request) {
/**
* Handle a request for a bms table. This endpoint should return "HTML" with the caveat
* that atleast one of the lines should refer to a "bmstable" meta header.
* that at least one of the lines should refer to a "bmstable" meta header.
*/
export function HandleBMSTableHTMLRequest(bmsTable: TachiBMSTable, req: Request, res: Response) {
let absURL;
@@ -417,7 +417,7 @@ export const CUSTOM_TACHI_BMS_TABLES: Array<TachiBMSTable> = [
promises.push(
(async () => {
const scores = await GetRecentUGPTScores(rival.id, game);
const scores = await GetRecentUGPTHighlights(rival.id, game);
const data = await GetRelevantSongsAndCharts(scores);
const charts = data.charts as unknown as Array<
@@ -26,7 +26,7 @@ export type TachiIIDXPlaylist = {
| {
forSpecificUser: true; // playlist is user dependent.
// like, say, their rivals scores or something.
// then the callbacks need to recieve that info.
// then the callbacks need to receive that info.
getPlaylists: (
userID: integer,
game: "iidx-dp" | "iidx-sp",
@@ -91,7 +91,7 @@ export class InvalidScoreFailure extends ConverterFailure {
}
/**
* KTInternalFailure - An unexpected, internal error has occured,
* KTInternalFailure - An unexpected, internal error has occurred,
* and the score could not be processed.
*/
export class InternalFailure extends ConverterFailure {
@@ -104,7 +104,7 @@ export async function ExpressWrappedScoreImportMain<I extends ImportTypes>(
statusCode: 500,
body: {
success: false,
description: "An internal service error has occured. This has been reported!",
description: "An internal service error has occurred. This has been reported!",
},
};
}
@@ -162,7 +162,7 @@ export async function ImportIterableDatapoint<D, C>(
return {
success: false,
type: "InternalError",
message: "An internal service error has occured.",
message: "An internal service error has occurred.",
content: {},
};
}
@@ -245,13 +245,13 @@ export async function ImportIterableDatapoint<D, C>(
}
case "Internal": {
log.error({ err: ClassToObject(err) }, `Internal error occured.`);
log.error({ err: ClassToObject(err) }, `Internal error occurred.`);
return {
success: false,
type: "InternalError",
// could return cfnReturn.message here, but we might want to hide the details of the crash.
message: "An internal error has occured.",
message: "An internal error has occurred.",
content: {},
};
}
@@ -284,7 +284,7 @@ export async function ImportIterableDatapoint<D, C>(
return {
success: false,
type: "InternalError",
message: "An internal service error has occured.",
message: "An internal service error has occurred.",
content: {},
};
}
@@ -226,7 +226,7 @@ export async function LoadScoresIntoSessions(
}
// There's no state here where curGroup is empty,
// so push the group (which is guaranteed to have atleast one score)
// so push the group (which is guaranteed to have at least one score)
sessionScoreGroups.push(curGroup);
log.debug(`Created ${sessionScoreGroups.length} groups from timestamped scores.`);
@@ -54,7 +54,7 @@ export async function CreateKaiIIDXClassProvider(
return (game, _userID, _ratings, log) => {
if (err !== undefined) {
log.error({ err }, `An error occured while updating classes for ${baseUrl}.`);
log.error({ err }, `An error occurred while updating classes for ${baseUrl}.`);
return {};
}
@@ -87,7 +87,7 @@ describe("#CreateKaiReauthFunction", () => {
const reauthFn = CreateKaiReauthFunction("FLO", authDoc, log, mockFetch);
await expect(reauthFn()).rejects.toMatchObject({
message: "An error has occured while attempting reauthentication.",
message: "An error has occurred while attempting reauthentication.",
});
const dbChange = await DB.selectFrom("priv_svc_kai_auth_token")
@@ -123,7 +123,7 @@ describe("#CreateKaiReauthFunction", () => {
const reauthFn = CreateKaiReauthFunction("FLO", authDoc, log, mockFetch);
await expect(reauthFn()).rejects.toMatchObject({
message: "An error has occured while attempting reauthentication.",
message: "An error has occurred while attempting reauthentication.",
});
const dbChange = await DB.selectFrom("priv_svc_kai_auth_token")
@@ -60,7 +60,7 @@ export function CreateKaiReauthFunction(
log.error({ res, err }, `Unexpected error while fetching reauth?`);
throw new ScoreImportFatalError(
500,
"An error has occured while attempting reauthentication.",
"An error has occurred while attempting reauthentication.",
);
}
@@ -85,7 +85,7 @@ export function CreateKaiReauthFunction(
log.error({ res, text }, `Unexpected ${res.status} error while fetching reauth?`);
throw new ScoreImportFatalError(
500,
"An error has occured while attempting reauthentication.",
"An error has occurred while attempting reauthentication.",
);
}
@@ -98,7 +98,7 @@ export function CreateKaiReauthFunction(
log.error({ res, err }, `Invalid JSON body in successful reauth response.`);
throw new ScoreImportFatalError(
500,
"An error has occured while attempting reauthentication.",
"An error has occurred while attempting reauthentication.",
);
}
@@ -108,7 +108,7 @@ export function CreateKaiReauthFunction(
log.error({ err, json }, `Invalid JSON body in successful reauth response.`);
throw new ScoreImportFatalError(
500,
"An error has occured while attempting reauthentication.",
"An error has occurred while attempting reauthentication.",
);
}
@@ -53,7 +53,7 @@ export async function CreateKaiSDVXClassProvider(
);
if (err !== undefined) {
log.error({ err }, `An error occured while updating classes for ${baseUrl}.`);
log.error({ err }, `An error occurred while updating classes for ${baseUrl}.`);
return {};
}
@@ -154,11 +154,11 @@ export async function* TraverseKaiAPI(
if (currentIteration > MAX_ITERATIONS) {
log.error(
`An infinite loop has occured - Terminating at MAX_ITERATIONS (${MAX_ITERATIONS}).`,
`An infinite loop has occurred - Terminating at MAX_ITERATIONS (${MAX_ITERATIONS}).`,
);
throw new ScoreImportFatalError(
508,
"An infinite loop appears to have occured while synchronising with this api.",
"An infinite loop appears to have occurred while synchronising with this api.",
);
}
@@ -146,7 +146,7 @@ async function HandleOrphanChartProcess(
}
// NOTE: This converter handles both PMS and BMS scores. The two are very similar,
// infact, beatoraja barely does anything different between the two. PMS is essentially
// in fact, beatoraja barely does anything different between the two. PMS is essentially
// BMS but with the columns set to 9.
export const ConverterIRBeatoraja: ConverterFunction<BeatorajaScore, BeatorajaContext> = async (
data,
@@ -5,16 +5,16 @@ import type { Parsers } from "../import-types/parsers";
// Ok so, please hear me out on this one.
// We need a type that removes the last element from an array.
// This is because parser functions have variable signatures, but all
// end with requiring a log. We don't want to have to pass thelog,
// end with requiring a log. We don't want to have to pass the logger,
// we just want to pass those other arguments.
type RemoveLast<T extends Array<unknown>> = T extends [...infer RemoveLast, unknown]
? RemoveLast
: Array<unknown>;
// Which means we can use generic access to make ParserArguments<"ir/usc">
// A type that returns the paramaters of the parser function bound to
// A type that returns the parameters of the parser function bound to
// "ir/usc".
// And then, using RemoveLast, we can remove thatlog argument we don't
// And then, using RemoveLast, we can remove that logger argument we don't
// want to provide.
export type ParserArguments<I extends ImportTypes> = RemoveLast<Parameters<(typeof Parsers)[I]>>;
+1 -1
View File
@@ -86,7 +86,7 @@ export class DatabaseSeedsRepo {
* to the repository.
*
* @param commitMsg - The commit message.
* @returns True when a commit has occured, false when it hasn't. Throws on failure.
* @returns True when a commit has occurred, false when it hasn't. Throws on failure.
*/
async CommitChangesBack(commitMsg: string) {
log.debug(`Received commit-back request.`);
@@ -203,7 +203,7 @@ export async function ValidateGoalChartsAndCriteria(
case "multi": {
if (charts.data.length < 2) {
throw new Error(
`Invalid charts.data for 'multi' charts. Must specify atleast two charts.`,
`Invalid charts.data for 'multi' charts. Must specify at least two charts.`,
);
}
@@ -157,7 +157,7 @@ export const RequirePermissions =
return res.status(500).json({
success: false,
description: "An internal error has occured.",
description: "An internal error has occurred.",
});
}
@@ -204,7 +204,7 @@ const CreateRequireNotGuest =
log.error(`RequirePermissions middleware was hit without any TachiAPIData?`);
return res.status(500).json({
success: false,
description: "An internal error has occured.",
description: "An internal error has occurred.",
});
}
@@ -30,7 +30,7 @@ export const CreateMulterSingleUploadMiddleware = (
return res.status(500).json({
success: false,
description: `An internal server error has occured.`,
description: `An internal server error has occurred.`,
});
}
@@ -67,7 +67,7 @@ API_V1_ROUTER.add("POST /auth/login", aggressiveRL, async ({ input, req, res })
{ requestedUser },
`State desync for user ${FormatUserDoc(requestedUser)}. This user has no password/email information?`,
);
throw new ExpectedErr(500, "An internal server error has occured.");
throw new ExpectedErr(500, "An internal server error has occurred.");
}
const passwordMatch = await PasswordCompare(input["!password"], privateInfo.password);
@@ -81,7 +81,7 @@ API_V1_ROUTER.add("POST /auth/login", aggressiveRL, async ({ input, req, res })
if (!user) {
log.error({ requestedUser }, `User logged in as someone who does not exist?`);
throw new ExpectedErr(500, "An internal server error has occured.");
throw new ExpectedErr(500, "An internal server error has occurred.");
}
const settings = await GetSettingsForUser(requestedUser.id);
@@ -117,7 +117,7 @@ API_V1_ROUTER.add("POST /auth/register", aggressiveRL, async ({ input, req }) =>
log.error(
`User ${newUser.userID} does not have a user document, but one was just created.`,
);
throw new ExpectedErr(500, "An internal server error has occured.");
throw new ExpectedErr(500, "An internal server error has occurred.");
}
const settings = await GetSettingsForUser(user.id);
@@ -432,7 +432,7 @@ API_V1_ROUTER.add("GET /games/:game/charts/:chartID", withGame, withChart, async
log.error(
`Song ${chart.song.id} does not exist, yet chart ${chart.chartID} has it as a parent?`,
);
throw new ExpectedErr(500, "An internal server error has occured.");
throw new ExpectedErr(500, "An internal server error has occurred.");
}
return success(`Returned chart.`, { chart, song: songRes.doc });
@@ -375,7 +375,7 @@ API_V1_ROUTER.add(
if (!privateInfo) {
log.error({ user }, `State desync for user ${FormatUserDoc(user)}.`);
throw new ExpectedErr(500, "An internal server error has occured.");
throw new ExpectedErr(500, "An internal server error has occurred.");
}
const password = input["!password"] as string;
@@ -109,7 +109,7 @@ API_V1_ROUTER.add(
log.error(
`Attempted to /callback ${kaiType}, but this server has no oauth2 credentials configured for that type.`,
);
throw new ExpectedErr(500, "A fatal error has occured, This has been reported.");
throw new ExpectedErr(500, "A fatal error has occurred, This has been reported.");
}
const { CLIENT_SECRET, CLIENT_ID, REDIRECT_URI } = maybeCredentials;
@@ -47,7 +47,7 @@ router.post("/submit-score", RequireNotGuest, async (req, res) => {
const errMsg = importRes.body.body.errors[0].message;
// If the error type is SongOrChartNotFound, then we **know** that
// the chart and score values were atleast typed correctly
// the chart and score values were at least typed correctly
// and can afford to make this assertion.
if (type === "SongOrChartNotFound") {
const { chart } = req.safeBody as { chart: BeatorajaChart };
@@ -369,7 +369,7 @@ router.post("/scores", RequirePermissions("submit_score"), async (req, res) => {
} catch (_err) {
return res.status(200).json({
statusCode: STATUS_CODES.SERVER_ERROR,
description: "An internal server error has occured.",
description: "An internal server error has occurred.",
});
}
});
@@ -435,7 +435,7 @@ router.post(
/* istanbul ignore next */
return res.status(200).json({
statusCode: STATUS_CODES.SERVER_ERROR,
description: "An error has occured in storing the replay.",
description: "An error has occurred in storing the replay.",
});
}
},
+2 -2
View File
@@ -144,7 +144,7 @@ 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 as atleast an object.
// always defined as at least an object.
if (req.method !== "GET" && (typeof req.body !== "object" || req.body === null)) {
req.body = {};
}
@@ -234,7 +234,7 @@ const MAIN_ERR_HANDLER: express.ErrorRequestHandler = (err, req, res, _next) =>
return res.status(500).json({
success: false,
description: "A fatal internal server error has occured.",
description: "A fatal internal server error has occurred.",
});
};
@@ -61,7 +61,7 @@ export function NaiveCSVParse(csvBuffer: Buffer, log: KtLogger) {
const cells = data.split(",").map((e) => {
// we want to remove quotes from anything that is *absolutely*
// surrounded by quotes.
// Even though we are naively parsing csvs, it seems like atleast
// Even though we are naively parsing csvs, it seems like at least
// one person managed to mangle their inputs such that this happened.
const isSurroundedByQuotes = /^"(.*)"$/u.exec(e) as [string, string] | null;