mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-11 08:48:15 +03:00
experimental: initial autofix blast
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import deepmerge from "deepmerge";
|
||||
import db from "external/mongo/db";
|
||||
import { Game, integer, Playtype, Playtypes, PublicUserDocument } from "tachi-common";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { CreateCalculatedData } from "lib/score-import/framework/calculated-data/calculated-data";
|
||||
import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/user-game-stats/update-ugs";
|
||||
import { GetAndUpdateUsersGoals } from "lib/score-import/framework/goals/goals";
|
||||
import { UpdateUsersMilestones } from "lib/score-import/framework/milestones/milestones";
|
||||
import { EfficientDBIterate } from "utils/efficient-db-iterate";
|
||||
import { ProcessPBs } from "lib/score-import/framework/pb/process-pbs";
|
||||
import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/user-game-stats/update-ugs";
|
||||
import { Playtypes } from "tachi-common";
|
||||
import { EfficientDBIterate } from "utils/efficient-db-iterate";
|
||||
import { FormatUserDoc } from "utils/user";
|
||||
import deepmerge from "deepmerge";
|
||||
import type { Game, integer, Playtype, PublicUserDocument } from "tachi-common";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -71,13 +72,7 @@ export async function RecalcAllScores(filter = {}) {
|
||||
|
||||
const userID = Number(strUserID);
|
||||
|
||||
await UpdateUsersGamePlaytypeStats(
|
||||
game as Game,
|
||||
playtype as Playtype,
|
||||
userID,
|
||||
null,
|
||||
logger
|
||||
);
|
||||
await UpdateUsersGamePlaytypeStats(game, playtype, userID, null, logger);
|
||||
|
||||
const goalInfo = await GetAndUpdateUsersGoals(game, userID, chartIDs, logger);
|
||||
|
||||
@@ -87,8 +82,8 @@ export async function RecalcAllScores(filter = {}) {
|
||||
logger.info(`Done!`);
|
||||
}
|
||||
|
||||
export async function UpdateAllPBs(userIDs?: integer[], filter = {}) {
|
||||
let allUsers: PublicUserDocument[];
|
||||
export async function UpdateAllPBs(userIDs?: Array<integer>, filter = {}) {
|
||||
let allUsers: Array<PublicUserDocument>;
|
||||
|
||||
if (!userIDs) {
|
||||
allUsers = await db.users.find({});
|
||||
|
||||
@@ -8,6 +8,7 @@ const logger = CreateLogCtx(__filename);
|
||||
|
||||
export async function RecalcSessions(filter = {}) {
|
||||
const allSessions = await db.sessions.find(filter);
|
||||
|
||||
logger.info(`Recalcing ${allSessions.length} sessions.`);
|
||||
|
||||
for (const session of allSessions) {
|
||||
@@ -19,6 +20,7 @@ export async function RecalcSessions(filter = {}) {
|
||||
);
|
||||
|
||||
let c;
|
||||
|
||||
try {
|
||||
c = CreateSessionCalcData(session.game, session.playtype, scores);
|
||||
} catch (err) {
|
||||
|
||||
+16
-16
@@ -2,8 +2,8 @@ import db from "external/mongo/db";
|
||||
import { CreateGameSettings } from "lib/game-settings/create-game-settings";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { EmitWebhookEvent } from "lib/webhooks/webhooks";
|
||||
import { Game, IDStrings, integer, Playtype, UserGameStats } from "tachi-common";
|
||||
import { GameClassSets } from "tachi-common/js/game-classes";
|
||||
import type { Game, IDStrings, integer, Playtype, UserGameStats } from "tachi-common";
|
||||
import type { GameClassSets } from "tachi-common/js/game-classes";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
@@ -86,19 +86,19 @@ export async function UpdateClassIfGreater(
|
||||
});
|
||||
|
||||
return null;
|
||||
} else {
|
||||
EmitWebhookEvent({
|
||||
type: "class-update/v1",
|
||||
content: {
|
||||
userID,
|
||||
new: classVal,
|
||||
old: userGameStats!.classes[classSet]!,
|
||||
set: classSet,
|
||||
game,
|
||||
playtype,
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
EmitWebhookEvent({
|
||||
type: "class-update/v1",
|
||||
content: {
|
||||
userID,
|
||||
new: classVal,
|
||||
old: userGameStats!.classes[classSet]!,
|
||||
set: classSet,
|
||||
game,
|
||||
playtype,
|
||||
},
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
+33
-40
@@ -1,8 +1,8 @@
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { FilterQuery } from "mongodb";
|
||||
import {
|
||||
FormatChart,
|
||||
import { FormatChart } from "tachi-common";
|
||||
import type { FilterQuery } from "mongodb";
|
||||
import type {
|
||||
Game,
|
||||
GoalDocument,
|
||||
GoalSubscriptionDocument,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
PBScoreDocument,
|
||||
ScoreDocument,
|
||||
} from "tachi-common";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
export async function GetNextCounterValue(counterName: string): Promise<integer> {
|
||||
@@ -66,7 +67,7 @@ export async function DecrementCounterValue(counterName: string): Promise<intege
|
||||
}
|
||||
|
||||
export async function GetRelevantSongsAndCharts(
|
||||
scores: (ScoreDocument | PBScoreDocument)[],
|
||||
scores: Array<PBScoreDocument | ScoreDocument>,
|
||||
game: Game
|
||||
) {
|
||||
const [songs, charts] = await Promise.all([
|
||||
@@ -177,13 +178,11 @@ export async function GetRecentlyAchievedGoals(
|
||||
baseQuery: FilterQuery<GoalSubscriptionDocument>,
|
||||
limit = 100
|
||||
) {
|
||||
const query = Object.assign(
|
||||
{
|
||||
wasInstantlyAchieved: false,
|
||||
achieved: true,
|
||||
},
|
||||
baseQuery
|
||||
);
|
||||
const query = {
|
||||
wasInstantlyAchieved: false,
|
||||
achieved: true,
|
||||
...baseQuery,
|
||||
};
|
||||
|
||||
const goalSubs = await db["goal-subs"].find(query, {
|
||||
sort: {
|
||||
@@ -218,14 +217,12 @@ export async function GetRecentlyInteractedGoals(
|
||||
baseQuery: FilterQuery<GoalSubscriptionDocument>,
|
||||
limit = 100
|
||||
) {
|
||||
const query = Object.assign(
|
||||
{
|
||||
wasInstantlyAchieved: false,
|
||||
achieved: false,
|
||||
lastInteraction: { $ne: null },
|
||||
},
|
||||
baseQuery
|
||||
);
|
||||
const query = {
|
||||
wasInstantlyAchieved: false,
|
||||
achieved: false,
|
||||
lastInteraction: { $ne: null },
|
||||
...baseQuery,
|
||||
};
|
||||
|
||||
const goalSubs = await db["goal-subs"].find(query, {
|
||||
sort: {
|
||||
@@ -260,13 +257,11 @@ export async function GetRecentlyAchievedMilestones(
|
||||
baseQuery: FilterQuery<MilestoneSubscriptionDocument>,
|
||||
limit = 100
|
||||
) {
|
||||
const query = Object.assign(
|
||||
{
|
||||
wasInstantlyAchieved: false,
|
||||
achieved: true,
|
||||
},
|
||||
baseQuery
|
||||
);
|
||||
const query = {
|
||||
wasInstantlyAchieved: false,
|
||||
achieved: true,
|
||||
...baseQuery,
|
||||
};
|
||||
|
||||
const milestoneSubs = await db["milestone-subs"].find(query, {
|
||||
sort: {
|
||||
@@ -301,14 +296,12 @@ export async function GetRecentlyInteractedMilestones(
|
||||
baseQuery: FilterQuery<MilestoneSubscriptionDocument>,
|
||||
limit = 100
|
||||
) {
|
||||
const query = Object.assign(
|
||||
{
|
||||
lastInteraction: { $ne: null },
|
||||
achieved: false,
|
||||
wasInstantlyAchieved: false,
|
||||
},
|
||||
baseQuery
|
||||
);
|
||||
const query = {
|
||||
lastInteraction: { $ne: null },
|
||||
achieved: false,
|
||||
wasInstantlyAchieved: false,
|
||||
...baseQuery,
|
||||
};
|
||||
|
||||
const milestoneSubs = await db["milestone-subs"].find(query, {
|
||||
sort: {
|
||||
@@ -335,8 +328,8 @@ export async function GetRecentlyInteractedMilestones(
|
||||
export async function GetMostSubscribedGoals(
|
||||
query: FilterQuery<GoalSubscriptionDocument>,
|
||||
limit = 100
|
||||
): Promise<(GoalDocument & { __subscriptions: integer })[]> {
|
||||
const mostSubscribedGoals = (await db["goal-subs"].aggregate([
|
||||
): Promise<Array<GoalDocument & { __subscriptions: integer }>> {
|
||||
const mostSubscribedGoals = await db["goal-subs"].aggregate([
|
||||
{
|
||||
$match: query,
|
||||
},
|
||||
@@ -370,7 +363,7 @@ export async function GetMostSubscribedGoals(
|
||||
{
|
||||
$unset: "goal._id",
|
||||
},
|
||||
])) as { goal: GoalDocument; subscriptions: integer }[];
|
||||
]);
|
||||
|
||||
return mostSubscribedGoals.map((e) => ({
|
||||
__subscriptions: e.subscriptions,
|
||||
@@ -381,8 +374,8 @@ export async function GetMostSubscribedGoals(
|
||||
export async function GetMostSubscribedMilestones(
|
||||
query: FilterQuery<MilestoneSubscriptionDocument>,
|
||||
limit = 100
|
||||
): Promise<(MilestoneDocument & { __subscriptions: integer })[]> {
|
||||
const mostSubscribedMilesones = (await db["milestone-subs"].aggregate([
|
||||
): Promise<Array<MilestoneDocument & { __subscriptions: integer }>> {
|
||||
const mostSubscribedMilesones = await db["milestone-subs"].aggregate([
|
||||
{
|
||||
$match: query,
|
||||
},
|
||||
@@ -416,7 +409,7 @@ export async function GetMostSubscribedMilestones(
|
||||
{
|
||||
$unset: "milestone._id",
|
||||
},
|
||||
])) as { milestone: MilestoneDocument; subscriptions: integer }[];
|
||||
]);
|
||||
|
||||
return mostSubscribedMilesones.map((e) => ({
|
||||
__subscriptions: e.subscriptions,
|
||||
|
||||
@@ -5,7 +5,7 @@ export function ParseEA3SoftID(ver: string) {
|
||||
throw new Error(`Invalid Version Code. Had ${a.length} components.`);
|
||||
}
|
||||
|
||||
if (!ver.match(/^[A-Z0-9]{3}:[A-Z]:[A-Z]:[A-Z]:[0-9]{10}$/u)) {
|
||||
if (!/^[A-Z0-9]{3}:[A-Z]:[A-Z]:[A-Z]:[0-9]{10}$/u.exec(ver)) {
|
||||
throw new Error(`Invalid Version Code.`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { ICollection } from "monk";
|
||||
import type { ICollection } from "monk";
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
export async function EfficientDBIterate<T, R>(
|
||||
collection: ICollection<T>,
|
||||
callbackFn: (c: T) => Promise<R>,
|
||||
saveOp: (c: R[]) => Promise<void>,
|
||||
saveOp: (c: Array<R>) => Promise<void>,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
filter: object = {},
|
||||
bucketSize = 10_000
|
||||
@@ -26,7 +26,7 @@ export async function EfficientDBIterate<T, R>(
|
||||
|
||||
const rDocs = await Promise.all(docs.map(callbackFn));
|
||||
|
||||
i += bucketSize;
|
||||
i = i + bucketSize;
|
||||
|
||||
await saveOp(rDocs);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Environment } from "lib/setup/config";
|
||||
import nodeFetch, { RequestInfo, RequestInit, Response } from "node-fetch";
|
||||
import nodeFetch from "node-fetch";
|
||||
import type { RequestInfo, RequestInit, Response } from "node-fetch";
|
||||
|
||||
const fetch =
|
||||
Environment.nodeEnv === "test"
|
||||
|
||||
+20
-14
@@ -3,8 +3,8 @@ import db from "external/mongo/db";
|
||||
import fjsh from "fast-json-stable-hash";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { TachiConfig } from "lib/setup/config";
|
||||
import { FilterQuery } from "mongodb";
|
||||
import {
|
||||
import type { FilterQuery } from "mongodb";
|
||||
import type {
|
||||
ChartDocument,
|
||||
FolderDocument,
|
||||
Game,
|
||||
@@ -26,19 +26,19 @@ export async function ResolveFolderToCharts(
|
||||
folder: FolderDocument,
|
||||
filter: FilterQuery<ChartDocument>,
|
||||
getSongs: true
|
||||
): Promise<{ songs: SongDocument[]; charts: ChartDocument[] }>;
|
||||
): Promise<{ songs: Array<SongDocument>; charts: Array<ChartDocument> }>;
|
||||
export async function ResolveFolderToCharts(
|
||||
folder: FolderDocument,
|
||||
filter?: FilterQuery<ChartDocument>,
|
||||
getSongs?: false
|
||||
): Promise<{ charts: ChartDocument[] }>;
|
||||
): Promise<{ charts: Array<ChartDocument> }>;
|
||||
export async function ResolveFolderToCharts(
|
||||
folder: FolderDocument,
|
||||
filter: FilterQuery<ChartDocument> = {},
|
||||
getSongs = false
|
||||
): Promise<{ songs?: SongDocument[]; charts: ChartDocument[] }> {
|
||||
let songs: SongDocument[] | null = null;
|
||||
let charts: ChartDocument[];
|
||||
): Promise<{ songs?: Array<SongDocument>; charts: Array<ChartDocument> }> {
|
||||
let songs: Array<SongDocument> | null = null;
|
||||
let charts: Array<ChartDocument>;
|
||||
|
||||
if (folder.type === "static") {
|
||||
charts = await db.charts[folder.game].find(
|
||||
@@ -115,17 +115,17 @@ export async function GetFolderCharts(
|
||||
folder: FolderDocument,
|
||||
filter: FilterQuery<ChartDocument>,
|
||||
getSongs: true
|
||||
): Promise<{ songs: SongDocument[]; charts: ChartDocument[] }>;
|
||||
): Promise<{ songs: Array<SongDocument>; charts: Array<ChartDocument> }>;
|
||||
export async function GetFolderCharts(
|
||||
folder: FolderDocument,
|
||||
filter: FilterQuery<ChartDocument>,
|
||||
getSongs: false
|
||||
): Promise<{ charts: ChartDocument[] }>;
|
||||
): Promise<{ charts: Array<ChartDocument> }>;
|
||||
export async function GetFolderCharts(
|
||||
folder: FolderDocument,
|
||||
filter: FilterQuery<ChartDocument> = {},
|
||||
getSongs = false
|
||||
): Promise<{ songs?: SongDocument[]; charts: ChartDocument[] }> {
|
||||
): Promise<{ songs?: Array<SongDocument>; charts: Array<ChartDocument> }> {
|
||||
const chartIDs = await GetFolderChartIDs(folder.folderID);
|
||||
|
||||
const charts = await db.charts[folder.game].find(
|
||||
@@ -157,6 +157,7 @@ export async function GetFolderChartIDs(folderID: string) {
|
||||
|
||||
return chartIDs.map((e) => e.chartID);
|
||||
}
|
||||
|
||||
export async function CreateFolderChartLookup(folder: FolderDocument, flush = false) {
|
||||
try {
|
||||
const { charts } = await ResolveFolderToCharts(folder, {}, false);
|
||||
@@ -194,6 +195,7 @@ export async function InitaliseFolderChartLookup() {
|
||||
const folders = await db.folders.find({
|
||||
game: { $in: TachiConfig.GAMES },
|
||||
});
|
||||
|
||||
logger.info(`Reloading ${folders.length} folders.`);
|
||||
|
||||
await Promise.all(folders.map((folder) => CreateFolderChartLookup(folder)));
|
||||
@@ -230,7 +232,7 @@ export async function GetPBsOnFolder(userID: integer, folder: FolderDocument) {
|
||||
return { pbs, charts, songs };
|
||||
}
|
||||
|
||||
export function CalculateLampDistribution(pbs: PBScoreDocument[]) {
|
||||
export function CalculateLampDistribution(pbs: Array<PBScoreDocument>) {
|
||||
const lampDist: Partial<Record<Lamps[IDStrings], integer>> = {};
|
||||
|
||||
for (const pb of pbs) {
|
||||
@@ -245,7 +247,7 @@ export function CalculateLampDistribution(pbs: PBScoreDocument[]) {
|
||||
return lampDist;
|
||||
}
|
||||
|
||||
export function CalculateGradeDistribution(pbs: PBScoreDocument[]) {
|
||||
export function CalculateGradeDistribution(pbs: Array<PBScoreDocument>) {
|
||||
const gradeDist: Partial<Record<Grades[IDStrings], integer>> = {};
|
||||
|
||||
for (const pb of pbs) {
|
||||
@@ -259,6 +261,7 @@ export function CalculateGradeDistribution(pbs: PBScoreDocument[]) {
|
||||
|
||||
return gradeDist;
|
||||
}
|
||||
|
||||
export async function GetGradeLampDistributionForFolder(userID: integer, folder: FolderDocument) {
|
||||
const pbData = await GetPBsOnFolder(userID, folder);
|
||||
|
||||
@@ -270,12 +273,15 @@ export async function GetGradeLampDistributionForFolder(userID: integer, folder:
|
||||
};
|
||||
}
|
||||
|
||||
export function GetGradeLampDistributionForFolders(userID: integer, folders: FolderDocument[]) {
|
||||
export function GetGradeLampDistributionForFolders(
|
||||
userID: integer,
|
||||
folders: Array<FolderDocument>
|
||||
) {
|
||||
return Promise.all(folders.map((f) => GetGradeLampDistributionForFolder(userID, f)));
|
||||
}
|
||||
|
||||
export function CreateFolderID(query: Record<string, unknown>, game: Game, playtype: Playtype) {
|
||||
return `F${fjsh.hash(Object.assign({ game, playtype }, query), "SHA256")}`;
|
||||
return `F${fjsh.hash({ game, playtype, ...query }, "SHA256")}`;
|
||||
}
|
||||
|
||||
export async function GetRecentlyViewedFolders(userID: integer, game: Game, playtype: Playtype) {
|
||||
|
||||
+24
-16
@@ -1,9 +1,10 @@
|
||||
import { exec } from "child_process";
|
||||
import crypto from "crypto";
|
||||
import { ONE_HOUR } from "lib/constants/time";
|
||||
import { TachiConfig } from "lib/setup/config";
|
||||
import { Game, GamePTConfig, GetGameConfig, integer, Playtype } from "tachi-common";
|
||||
import { GetGameConfig } from "tachi-common";
|
||||
import { exec } from "child_process";
|
||||
import crypto from "crypto";
|
||||
import { URL } from "url";
|
||||
import type { Game, GamePTConfig, integer, Playtype } from "tachi-common";
|
||||
|
||||
// https://github.com/sindresorhus/escape-string-regexp/blob/main/index.js
|
||||
// the developer of this has migrated everything to Force ES6 style modules,
|
||||
@@ -39,7 +40,7 @@ export function MStoS(ms: number) {
|
||||
/**
|
||||
* Random From Array - Selects a random value from an array.
|
||||
*/
|
||||
export function RFA<T>(arr: T[]): T {
|
||||
export function RFA<T>(arr: Array<T>): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
@@ -60,12 +61,12 @@ export function SplitAuthorizationHeader(authHeader: string) {
|
||||
* key to keyof T.
|
||||
* False if the object doesn't.
|
||||
*/
|
||||
export function HasOwnProperty<T>(obj: T, key: string | number | symbol): key is keyof T {
|
||||
export function HasOwnProperty<T>(obj: T, key: number | string | symbol): key is keyof T {
|
||||
return Object.prototype.hasOwnProperty.call(obj, key);
|
||||
}
|
||||
|
||||
export function IsValidGame(str: string): str is Game {
|
||||
return !!TachiConfig.GAMES.includes(str as Game);
|
||||
return Boolean(TachiConfig.GAMES.includes(str as Game));
|
||||
}
|
||||
|
||||
export function IsValidPlaytype(game: Game, str: string): str is Playtype {
|
||||
@@ -83,12 +84,12 @@ export function IsString(val: unknown): val is string {
|
||||
return typeof val === "string";
|
||||
}
|
||||
|
||||
export function DedupeArr<T>(arr: T[]): T[] {
|
||||
export function DedupeArr<T>(arr: Array<T>): Array<T> {
|
||||
return [...new Set(arr)];
|
||||
}
|
||||
|
||||
export function StripUrl(url: string, userInput: string | null) {
|
||||
if (!userInput) {
|
||||
if (userInput === null || userInput === "") {
|
||||
return userInput;
|
||||
}
|
||||
|
||||
@@ -122,22 +123,28 @@ export function DeleteUndefinedProps(record: any) {
|
||||
export function IsValidURL(string: string) {
|
||||
try {
|
||||
const url = new URL(string);
|
||||
|
||||
return url.protocol === "http:" || url.protocol === "https:";
|
||||
} catch (err) {
|
||||
} catch (_err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function Sleep(ms: number) {
|
||||
return new Promise<void>((resolve) => setTimeout(() => resolve(), ms));
|
||||
return new Promise<void>((resolve) =>
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, ms)
|
||||
);
|
||||
}
|
||||
|
||||
export function GetTimeXHoursAgo(hours: integer) {
|
||||
return Date.now() - ONE_HOUR * hours;
|
||||
}
|
||||
|
||||
export function ApplyNTimes<T>(n: integer, fn: (i: integer) => T): T[] {
|
||||
export function ApplyNTimes<T>(n: integer, fn: (i: integer) => T): Array<T> {
|
||||
const arr = [];
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
arr.push(fn(i));
|
||||
}
|
||||
@@ -176,15 +183,16 @@ export function asyncExec(command: string) {
|
||||
exec(command, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
return reject({ err, stdout, stderr });
|
||||
reject({ err, stdout, stderr });
|
||||
return;
|
||||
}
|
||||
|
||||
return resolve({ stdout, stderr });
|
||||
resolve({ stdout, stderr });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function FormatBMSTables(bmsTables: { table: string; level: string }[]) {
|
||||
export function FormatBMSTables(bmsTables: Array<{ table: string; level: string }>) {
|
||||
if (bmsTables.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -192,7 +200,7 @@ export function FormatBMSTables(bmsTables: { table: string; level: string }[]) {
|
||||
return bmsTables.map((e) => `${e.table}${e.level}`).join(", ");
|
||||
}
|
||||
|
||||
export function HumanisedJoinArray(arr: string[], lastJoiner = "or") {
|
||||
export function HumanisedJoinArray(arr: Array<string>, lastJoiner = "or") {
|
||||
return `${arr.slice(0, arr.length - 1).join(", ")} ${lastJoiner} ${arr[arr.length - 1]}`;
|
||||
}
|
||||
|
||||
@@ -213,6 +221,6 @@ export function IsSupported(game: Game) {
|
||||
* @param right - The right side of elements; everything in here not in left will be
|
||||
* returned.
|
||||
*/
|
||||
export function ArrayDiff<T>(left: T[], right: T[]) {
|
||||
export function ArrayDiff<T>(left: Array<T>, right: Array<T>) {
|
||||
return right.filter((e) => !left.includes(e));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
|
||||
export class CSVParseError extends Error {
|
||||
constructor(description: string) {
|
||||
@@ -42,7 +42,7 @@ export function NaiveCSVParse(csvBuffer: Buffer, logger: KtLogger) {
|
||||
rawHeaders.push(curStr);
|
||||
curStr = "";
|
||||
} else {
|
||||
curStr += char;
|
||||
curStr = curStr + char;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import p, { PrudenceError, ValidSchemaValue } from "prudence";
|
||||
import p from "prudence";
|
||||
import type { PrudenceError, ValidSchemaValue } from "prudence";
|
||||
|
||||
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 ${err.userVal.toString()} [${
|
||||
err.userVal === null ? "null" : typeof err.userVal
|
||||
}]`;
|
||||
|
||||
return `${foreword}: ${err.keychain} | ${err.message}${receivedText}.`;
|
||||
return `${foreword}: ${err.keychain ?? "null"} | ${err.message}${receivedText}.`;
|
||||
}
|
||||
|
||||
export const optNull = (v: ValidSchemaValue) => p.optional(p.nullable(v));
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import db from "external/mongo/db";
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import ScoreImportFatalError from "lib/score-import/framework/score-importing/score-import-error";
|
||||
import { integer } from "tachi-common";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type { integer } from "tachi-common";
|
||||
|
||||
export function GetKaiAuth(userID: integer, service: "FLO" | "EAG" | "MIN") {
|
||||
export function GetKaiAuth(userID: integer, service: "EAG" | "FLO" | "MIN") {
|
||||
return db["kai-auth-tokens"].findOne({
|
||||
userID,
|
||||
service,
|
||||
@@ -12,7 +12,7 @@ export function GetKaiAuth(userID: integer, service: "FLO" | "EAG" | "MIN") {
|
||||
|
||||
export async function GetKaiAuthGuaranteed(
|
||||
userID: integer,
|
||||
service: "FLO" | "EAG" | "MIN",
|
||||
service: "EAG" | "FLO" | "MIN",
|
||||
logger: KtLogger
|
||||
) {
|
||||
const authDoc = await GetKaiAuth(userID, service);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import db from "external/mongo/db";
|
||||
import { FilterQuery } from "mongodb";
|
||||
import {
|
||||
import type { FilterQuery } from "mongodb";
|
||||
import type {
|
||||
ChartDocument,
|
||||
Difficulties,
|
||||
Game,
|
||||
@@ -26,9 +26,9 @@ export function FindChartWithPTDF<
|
||||
I extends IDStrings = IDStrings
|
||||
>(game: G, songID: integer, playtype: P, difficulty: Difficulties[I]) {
|
||||
return db.charts[game].findOne({
|
||||
songID: songID,
|
||||
playtype: playtype,
|
||||
difficulty: difficulty,
|
||||
songID,
|
||||
playtype,
|
||||
difficulty,
|
||||
isPrimary: true,
|
||||
});
|
||||
}
|
||||
@@ -49,9 +49,9 @@ export function FindChartWithPTDFVersion<
|
||||
version: GPTSupportedVersions[I]
|
||||
) {
|
||||
return db.charts[game].findOne({
|
||||
songID: songID,
|
||||
playtype: playtype,
|
||||
difficulty: difficulty,
|
||||
songID,
|
||||
playtype,
|
||||
difficulty,
|
||||
versions: version,
|
||||
});
|
||||
}
|
||||
@@ -69,6 +69,7 @@ export function FindChartWithPTDFVersion<
|
||||
*/
|
||||
export function FindDDRChartOnSongHash(
|
||||
songHash: string,
|
||||
|
||||
// Technically both of these should be "ddr" instead of Game, but it proves hard to work with.
|
||||
playtype: Playtype,
|
||||
difficulty: Difficulties[IDStrings]
|
||||
@@ -190,7 +191,7 @@ export function FindSDVXChartOnInGameID(
|
||||
) {
|
||||
const diffQuery =
|
||||
difficulty === "ANY_INF"
|
||||
? { $in: ["INF", "GRV", "HVN", "VVD"] as Difficulties["sdvx:Single"][] }
|
||||
? { $in: ["INF", "GRV", "HVN", "VVD"] as Array<Difficulties["sdvx:Single"]> }
|
||||
: difficulty;
|
||||
|
||||
return db.charts.sdvx.findOne({
|
||||
@@ -202,12 +203,12 @@ export function FindSDVXChartOnInGameID(
|
||||
|
||||
export function FindSDVXChartOnInGameIDVersion(
|
||||
inGameID: number,
|
||||
difficulty: "NOV" | "ADV" | "EXH" | "MXM" | "ANY_INF",
|
||||
difficulty: "ADV" | "ANY_INF" | "EXH" | "MXM" | "NOV",
|
||||
version: GPTSupportedVersions["sdvx:Single"]
|
||||
) {
|
||||
const diffQuery =
|
||||
difficulty === "ANY_INF"
|
||||
? { $in: ["INF", "GRV", "HVN", "VVD"] as Difficulties["sdvx:Single"][] }
|
||||
? { $in: ["INF", "GRV", "HVN", "VVD"] as Array<Difficulties["sdvx:Single"]> }
|
||||
: difficulty;
|
||||
|
||||
return db.charts.sdvx.findOne({
|
||||
@@ -219,12 +220,12 @@ export function FindSDVXChartOnInGameIDVersion(
|
||||
|
||||
export function FindSDVXChartOnDFVersion(
|
||||
songID: integer,
|
||||
difficulty: "NOV" | "ADV" | "EXH" | "MXM" | "ANY_INF",
|
||||
difficulty: "ADV" | "ANY_INF" | "EXH" | "MXM" | "NOV",
|
||||
version: GPTSupportedVersions["sdvx:Single"]
|
||||
) {
|
||||
const diffQuery =
|
||||
difficulty === "ANY_INF"
|
||||
? { $in: ["INF", "GRV", "HVN", "VVD"] as Difficulties["sdvx:Single"][] }
|
||||
? { $in: ["INF", "GRV", "HVN", "VVD"] as Array<Difficulties["sdvx:Single"]> }
|
||||
: difficulty;
|
||||
|
||||
return db.charts.sdvx.findOne({
|
||||
@@ -255,7 +256,7 @@ export function FindChartOnSHA256Playtype(game: Game, hash: string, playtype: Pl
|
||||
});
|
||||
}
|
||||
|
||||
export function FindChartOnARCID(game: "iidx" | "ddr" | "jubeat" | "sdvx", arcID: string) {
|
||||
export function FindChartOnARCID(game: "ddr" | "iidx" | "jubeat" | "sdvx", arcID: string) {
|
||||
return db.charts[game].findOne({
|
||||
"data.arcChartID": arcID,
|
||||
});
|
||||
@@ -269,11 +270,11 @@ export function FindChartOnARCID(game: "iidx" | "ddr" | "jubeat" | "sdvx", arcID
|
||||
export async function FindChartsOnPopularity(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
songIDs?: integer[],
|
||||
songIDs?: Array<integer>,
|
||||
skip = 0,
|
||||
limit = 100,
|
||||
scoreCollection: "personal-bests" | "scores" = "personal-bests"
|
||||
): Promise<(ChartDocument & { __playcount: integer })[]> {
|
||||
): Promise<Array<ChartDocument & { __playcount: integer }>> {
|
||||
const matchQuery: FilterQuery<ChartDocument> = {
|
||||
playtype,
|
||||
};
|
||||
@@ -290,11 +291,13 @@ export async function FindChartsOnPopularity(
|
||||
// magnitude faster.
|
||||
// Not entirely sure why, but $lookup is incredibly inefficient,
|
||||
// and you should just avoid it.
|
||||
const charts = (await db.charts[game].find(matchQuery)) as unknown as (ChartDocument & {
|
||||
__playcount: integer;
|
||||
})[];
|
||||
const charts = (await db.charts[game].find(matchQuery)) as unknown as Array<
|
||||
ChartDocument & {
|
||||
__playcount: integer;
|
||||
}
|
||||
>;
|
||||
|
||||
const scoreCounts = (await db[scoreCollection].aggregate([
|
||||
const scoreCounts = await db[scoreCollection].aggregate([
|
||||
{
|
||||
$match: { chartID: { $in: charts.map((e) => e.chartID) } },
|
||||
},
|
||||
@@ -315,7 +318,7 @@ export async function FindChartsOnPopularity(
|
||||
{
|
||||
$limit: limit,
|
||||
},
|
||||
])) as { _id: string; count: integer }[];
|
||||
]);
|
||||
|
||||
const scoreCountMap = new Map();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import db from "external/mongo/db";
|
||||
import { PBScoreDocument } from "tachi-common";
|
||||
import type { PBScoreDocument } from "tachi-common";
|
||||
|
||||
export async function GetAdjacentAbove(userPB: PBScoreDocument, size = 5) {
|
||||
const adjAbove = (await db["personal-bests"].find(
|
||||
@@ -11,7 +11,7 @@ export async function GetAdjacentAbove(userPB: PBScoreDocument, size = 5) {
|
||||
limit: size,
|
||||
sort: { "rankingData.rank": -1 },
|
||||
}
|
||||
)) as PBScoreDocument<"usc:Controller" | "usc:Keyboard">[];
|
||||
)) as Array<PBScoreDocument<"usc:Controller" | "usc:Keyboard">>;
|
||||
|
||||
return adjAbove;
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export async function GetAdjacentBelow(userPB: PBScoreDocument, size = 5) {
|
||||
limit: size,
|
||||
sort: { "rankingData.rank": 1 },
|
||||
}
|
||||
)) as PBScoreDocument<"usc:Controller" | "usc:Keyboard">[];
|
||||
)) as Array<PBScoreDocument<"usc:Controller" | "usc:Keyboard">>;
|
||||
|
||||
return adjAbove;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import db from "external/mongo/db";
|
||||
import { KtLogger } from "lib/logger/logger";
|
||||
import { InternalFailure } from "lib/score-import/framework/common/converter-failures";
|
||||
import { FindOneResult } from "monk";
|
||||
import { Game, integer, SongDocument } from "tachi-common";
|
||||
import { EscapeStringRegexp } from "../misc";
|
||||
import db from "external/mongo/db";
|
||||
import { InternalFailure } from "lib/score-import/framework/common/converter-failures";
|
||||
import type { KtLogger } from "lib/logger/logger";
|
||||
import type { FindOneResult } from "monk";
|
||||
import type { Game, integer, SongDocument } from "tachi-common";
|
||||
|
||||
/**
|
||||
* Finds a song document for the given game with the given title (or alt-title).
|
||||
@@ -18,7 +18,7 @@ export function FindSongOnTitle(game: Game, title: string): Promise<FindOneResul
|
||||
return db.songs[game].findOne({
|
||||
$or: [
|
||||
{
|
||||
title: title,
|
||||
title,
|
||||
},
|
||||
{
|
||||
altTitles: title,
|
||||
@@ -38,6 +38,7 @@ export function FindSongOnTitleInsensitive(
|
||||
// @optimisable: Performance should be tested here by having a utility field for all-titles.
|
||||
|
||||
const regex = new RegExp(`^${EscapeStringRegexp(title)}$`, "iu");
|
||||
|
||||
return db.songs[game].findOne({
|
||||
$or: [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import db from "external/mongo/db";
|
||||
import { ONE_HOUR } from "lib/constants/time";
|
||||
import NodeCache from "node-cache";
|
||||
import { Game, gameClasses, IDStrings, integer, Playtype, Playtypes } from "tachi-common";
|
||||
import type { Game, gameClasses, IDStrings, Playtype } from "tachi-common";
|
||||
|
||||
const classDistCache = new NodeCache();
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function GetClassDistribution(
|
||||
const cache = classDistCache.get(cacheKey);
|
||||
|
||||
if (!cache) {
|
||||
const distribution = (await db["game-stats"].aggregate([
|
||||
const distribution = await db["game-stats"].aggregate([
|
||||
{
|
||||
$match: {
|
||||
game,
|
||||
@@ -27,7 +27,7 @@ export async function GetClassDistribution(
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
])) as { _id: string; count: integer }[];
|
||||
]);
|
||||
|
||||
// Converts {_id: "kaiden", count: 3} to {"kaiden": 3}, more or less.
|
||||
const convert = Object.fromEntries(distribution.map((e) => [e._id, e.count]));
|
||||
@@ -35,7 +35,7 @@ export async function GetClassDistribution(
|
||||
classDistCache.set(cacheKey, convert, ONE_HOUR);
|
||||
|
||||
return convert;
|
||||
} else {
|
||||
return cache;
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import db from "external/mongo/db";
|
||||
import { integer } from "tachi-common";
|
||||
import { GetGradeLampDistributionForFolders } from "utils/folder";
|
||||
import { GetTimeXHoursAgo } from "utils/misc";
|
||||
import type { integer } from "tachi-common";
|
||||
|
||||
// Various utils related to the player summary endpoint.
|
||||
const REASONABLE_HOURS_AGO = 16;
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { SYMBOL_TachiData } from "lib/constants/tachi";
|
||||
import { Request } from "express";
|
||||
import { TachiRequestData } from "./types";
|
||||
import deepmerge from "deepmerge";
|
||||
import { SYMBOL_TACHI_DATA } from "lib/constants/tachi";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import type { TachiRequestData } from "./types";
|
||||
import type { Request } from "express";
|
||||
|
||||
export function AssignToReqTachiData(req: Request, data: Partial<TachiRequestData>) {
|
||||
if (!req[SYMBOL_TachiData]) {
|
||||
req[SYMBOL_TachiData] = data;
|
||||
if (!req[SYMBOL_TACHI_DATA]) {
|
||||
req[SYMBOL_TACHI_DATA] = data;
|
||||
} else {
|
||||
req[SYMBOL_TachiData] = deepmerge(req[SYMBOL_TachiData]!, data);
|
||||
req[SYMBOL_TACHI_DATA] = deepmerge(req[SYMBOL_TACHI_DATA]!, data);
|
||||
}
|
||||
}
|
||||
|
||||
const logger = CreateLogCtx(__filename);
|
||||
|
||||
export function GetUGPT(req: Request) {
|
||||
if (!req[SYMBOL_TachiData]) {
|
||||
if (!req[SYMBOL_TACHI_DATA]) {
|
||||
logger.error(`Tried to get UGPT from a req that doesn't have SYMBOL_TachiData.`);
|
||||
throw new Error("Failed to get UGPT.");
|
||||
}
|
||||
|
||||
const user = req[SYMBOL_TachiData]!.requestedUser;
|
||||
const game = req[SYMBOL_TachiData]!.game;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype;
|
||||
const user = req[SYMBOL_TACHI_DATA]!.requestedUser;
|
||||
const game = req[SYMBOL_TACHI_DATA]!.game;
|
||||
const playtype = req[SYMBOL_TACHI_DATA]!.playtype;
|
||||
|
||||
if (!user || !game || !playtype) {
|
||||
logger.error(`Tried to get UGPT from a req that doesnt have U, G and PT.`);
|
||||
@@ -33,13 +33,13 @@ export function GetUGPT(req: Request) {
|
||||
}
|
||||
|
||||
export function GetGPT(req: Request) {
|
||||
if (!req[SYMBOL_TachiData]) {
|
||||
if (!req[SYMBOL_TACHI_DATA]) {
|
||||
logger.error(`Tried to get GPT from a req that doesn't have SYMBOL_TachiData.`);
|
||||
throw new Error("Failed to get GPT.");
|
||||
}
|
||||
|
||||
const game = req[SYMBOL_TachiData]!.game;
|
||||
const playtype = req[SYMBOL_TachiData]!.playtype;
|
||||
const game = req[SYMBOL_TACHI_DATA]!.game;
|
||||
const playtype = req[SYMBOL_TACHI_DATA]!.playtype;
|
||||
|
||||
if (!game || !playtype) {
|
||||
logger.error(`Tried to get GPT from a req that doesnt have a Game and Playtype.`);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import db from "external/mongo/db";
|
||||
import { integer, Game, Playtypes, Playtype } from "tachi-common";
|
||||
import { Playtypes } from "tachi-common";
|
||||
import { UpdateAllPBs } from "utils/calculations/recalc-scores";
|
||||
import type { integer, Game, Playtype } from "tachi-common";
|
||||
|
||||
/**
|
||||
* Completely resets a UGPT profile.
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import db from "external/mongo/db";
|
||||
import { ChartDocument, integer, PBScoreDocument, ScoreDocument, SongDocument } from "tachi-common";
|
||||
import { DedupeArr } from "./misc";
|
||||
import db from "external/mongo/db";
|
||||
import type {
|
||||
ChartDocument,
|
||||
integer,
|
||||
PBScoreDocument,
|
||||
ScoreDocument,
|
||||
SongDocument,
|
||||
} from "tachi-common";
|
||||
|
||||
export function GetPBOnChart(userID: integer, chartID: string) {
|
||||
return db["personal-bests"].findOne({
|
||||
@@ -17,9 +23,9 @@ export function GetServerRecordOnChart(chartID: string) {
|
||||
}
|
||||
|
||||
export function FilterChartsAndSongs(
|
||||
scores: (PBScoreDocument | ScoreDocument)[],
|
||||
charts: ChartDocument[],
|
||||
songs: SongDocument[]
|
||||
scores: Array<PBScoreDocument | ScoreDocument>,
|
||||
charts: Array<ChartDocument>,
|
||||
songs: Array<SongDocument>
|
||||
) {
|
||||
const chartIDs = new Set();
|
||||
const songIDs = new Set();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import db from "external/mongo/db";
|
||||
import { Condition } from "mongodb";
|
||||
import { ScoreDocument, SessionDocument, SessionScoreInfo } from "tachi-common";
|
||||
import type { Condition } from "mongodb";
|
||||
import type { ScoreDocument, SessionDocument, SessionScoreInfo } from "tachi-common";
|
||||
|
||||
/**
|
||||
* Returns all the score documents inside a session.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Game, GamePTConfig, GetGamePTConfig, Playtype, Playtypes } from "tachi-common";
|
||||
import { IsString } from "./misc";
|
||||
import { GetGamePTConfig, Playtypes } from "tachi-common";
|
||||
import type { Game, GamePTConfig, Playtype } from "tachi-common";
|
||||
|
||||
const isIntegerRegex = /^-?\d+$/u;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {
|
||||
import type {
|
||||
FolderDocument,
|
||||
TableDocument,
|
||||
SessionDocument,
|
||||
@@ -20,8 +20,8 @@ import {
|
||||
ImportDocument,
|
||||
} from "tachi-common";
|
||||
|
||||
// Inject additional properties on express-session
|
||||
declare module "express-session" {
|
||||
// Inject additional properties on express-session
|
||||
interface SessionData {
|
||||
tachi: TachiSessionData;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import db from "external/mongo/db";
|
||||
import CreateLogCtx from "lib/logger/logger";
|
||||
import { FindOneResult } from "monk";
|
||||
import {
|
||||
import { GetGamePTConfig, UserAuthLevels } from "tachi-common";
|
||||
import type { FindOneResult } from "monk";
|
||||
import type {
|
||||
APITokenDocument,
|
||||
Game,
|
||||
GetGamePTConfig,
|
||||
IDStrings,
|
||||
integer,
|
||||
Playtype,
|
||||
PublicUserDocument,
|
||||
UGSRatingsLookup,
|
||||
UserAuthLevels,
|
||||
UserGameStats,
|
||||
} from "tachi-common";
|
||||
|
||||
@@ -46,13 +45,13 @@ export function GetUserCaseInsensitive(
|
||||
): Promise<FindOneResult<PublicUserDocument>> {
|
||||
return db.users.findOne({
|
||||
usernameLowercase: username.toLowerCase(),
|
||||
}) as Promise<FindOneResult<PublicUserDocument>>;
|
||||
});
|
||||
}
|
||||
|
||||
export async function CheckIfEmailInUse(email: string) {
|
||||
const doc = await db["user-private-information"].findOne({ email });
|
||||
|
||||
return !!doc;
|
||||
return Boolean(doc);
|
||||
}
|
||||
|
||||
export function GetUserPrivateInfo(userID: integer) {
|
||||
@@ -65,19 +64,19 @@ export function GetUserPrivateInfo(userID: integer) {
|
||||
export function GetUserWithID(userID: integer): Promise<FindOneResult<PublicUserDocument>> {
|
||||
return db.users.findOne({
|
||||
id: userID,
|
||||
}) as Promise<FindOneResult<PublicUserDocument>>;
|
||||
});
|
||||
}
|
||||
|
||||
export function GetSettingsForUser(userID: integer) {
|
||||
return db["user-settings"].findOne({
|
||||
userID: userID,
|
||||
userID,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the users for these user IDs.
|
||||
*/
|
||||
export async function GetUsersWithIDs(userIDs: integer[]) {
|
||||
export async function GetUsersWithIDs(userIDs: Array<integer>) {
|
||||
const users = await db.users.find({
|
||||
id: { $in: userIDs },
|
||||
});
|
||||
@@ -123,7 +122,7 @@ export async function GetUserWithIDGuaranteed(userID: integer): Promise<PublicUs
|
||||
*/
|
||||
export function ResolveUser(usernameOrID: string) {
|
||||
// user ID passed
|
||||
if (usernameOrID.match(/^[0-9]+$/u)) {
|
||||
if (/^[0-9]+$/u.exec(usernameOrID)) {
|
||||
const intID = Number(usernameOrID);
|
||||
|
||||
return GetUserWithID(intID);
|
||||
|
||||
Reference in New Issue
Block a user