Move scripts into src/

This commit is contained in:
zkldi
2022-02-09 16:19:45 +00:00
parent ce5a3e9ba1
commit 357aa7c2a8
20 changed files with 5 additions and 6 deletions
+6
View File
@@ -0,0 +1,6 @@
Hey. If by any chance I'm not available, you can (and should) run these scripts with TS-Node.
https://github.com/TypeStrong/ts-node
They have only been tested for ts-node.
pnpm install -g ts-node
@@ -0,0 +1,105 @@
import { execSync } from "child_process";
import { Command } from "commander";
import CreateLogCtx from "lib/logger/logger";
import monk from "monk";
const program = new Command();
program
.option("--nsFrom <Database to clone from.>")
.option("--nsTo <Database to clone and anonymise to.>");
program.parse(process.argv);
const options = program.opts();
if (!options.nsTo || !options.nsFrom) {
throw new Error(`Expected --nsFrom and --nsTo options.`);
}
const logger = CreateLogCtx(__filename);
/**
* Strips all name and private information from the database.
* For use for things like future tachi-db-dumps?
*
* @param nsTo The database to anonymise. DO NOT PASS THE PRODUCTION DATABASE TO THIS!
*/
async function AnonymiseDB(nsTo: string) {
logger.info(`Connecting to ${nsTo}.`);
const clonedDB = monk(`127.0.0.1/${nsTo}`);
logger.info(`Connected to ${nsTo}.`);
const r1 = await clonedDB.get("user-private-information").update(
{},
[
{
$set: {
// This is "password" encrypted in bcrypt12.
password: "$2b$12$QRFCAxvFoNI2spszFPgt/e.qLy55GvYWlSHioa0AujRbFpChLwHmu",
// emails are uniquely indexed. We need to anonymise these in
// a way that they dont duplicate.
email: { $concat: [{ $toString: "$userID" }, "@example.com"] },
},
},
],
{
multi: true,
}
);
logger.info(`Stripped private info.`, { r1 });
const r2 = await clonedDB.get("users").update(
{},
[
{
$set: {
socialMedia: { $const: {} },
},
},
{
$set: {
username: { $concat: ["u", { $toString: "$id" }] },
usernameLowercase: { $concat: ["u", { $toString: "$id" }] },
},
},
],
{
multi: true,
}
);
logger.info(`Stripped username info.`, { r2 });
for (const collection of [
"api-clients",
"oauth2-auth-codes",
"password-reset-codes",
"api-tokens",
"kai-auth-tokens",
"arc-saved-profiles",
]) {
// eslint-disable-next-line no-await-in-loop
const r3 = await clonedDB.get("collection").remove({});
logger.info(`Removed all ${collection} documents.`, { r3 });
}
logger.info(`Done! Closing.`);
process.exit(0);
}
function CloneDB(nsFrom: string, nsTo: string) {
logger.info(`Cloning Database. This will take a while.`);
execSync(`mongodump --gzip --archive=temp-dump --db=${nsFrom}`);
execSync(`mongorestore --archive=temp-dump --gzip --nsFrom='${nsFrom}.*' --nsTo='${nsTo}.*'`);
execSync(`rm temp-dump`);
logger.info(`Cloned!`);
}
if (require.main === module) {
CloneDB(options.nsFrom, options.nsTo);
AnonymiseDB(options.nsTo);
}
@@ -0,0 +1,30 @@
import { Command } from "commander";
import { SendEmail } from "lib/email/client";
import { MainHTMLWrapper } from "lib/email/formats";
import CreateLogCtx from "lib/logger/logger";
const program = new Command();
program.option("-e, --email <Email to send to>");
const logger = CreateLogCtx(__filename);
program.parse(process.argv);
const options = program.opts();
if (!options.email) {
throw new Error(`Need an --email to send to.`);
}
if (require.main === module) {
(async () => {
logger.info(`Sending email to ${options.email}.`);
await SendEmail(
options.email,
"Hello World",
MainHTMLWrapper("Hello world! This is a test email for doing things."),
"Hello world! This is a test email for doing things."
);
logger.info(`Done.`);
process.exit(0);
})();
}
@@ -0,0 +1,3 @@
import { InitaliseFolderChartLookup } from "../../utils/folder";
InitaliseFolderChartLookup().then(() => process.exit(0));
@@ -0,0 +1,180 @@
/* eslint-disable no-await-in-loop */
/* eslint-disable no-case-declarations */
/* eslint-disable no-param-reassign */
import db from "external/mongo/db";
import CreateLogCtx from "lib/logger/logger";
import fetch from "node-fetch";
import { Difficulties, integer } from "tachi-common";
import { FindChartWithPTDF } from "utils/queries/charts";
import { FindSongOnTitle } from "utils/queries/songs";
const logger = CreateLogCtx(__filename);
interface SP12Data {
id: integer;
title: string;
n_clear: integer;
hard: integer;
exh: integer;
n_clear_string: string;
hard_string: string;
exh_string: string;
version: integer;
}
async function FetchSP12Data() {
const rj: { sheets: SP12Data[] } = await fetch("https://sp12.iidx.app/api/v1/sheets").then(
(r) => r.json()
); // will throw if somethings wrong, anyway
if (!rj.sheets) {
throw new Error(`No sheets in return from sp12?`);
}
for (const sh of rj.sheets) {
let chart;
try {
chart = await HumanisedTitleLookup(sh.title);
} catch (err) {
logger.error((err as Error).message);
continue;
}
if (!chart) {
logger.warn(`Couldn't find chart with title ${sh.title}`);
}
for (const key of ["n_clear", "hard", "exh"] as const) {
let val;
switch (key) {
case "n_clear":
const v = Math.floor(sh[key] / 2);
if (v === 9) {
val = 11.8;
} else if (v === 8) {
val = 12.0;
} else if (v === 7) {
val = 12.2;
} else if (v === 6) {
val = 12.4;
} else if (v === 5) {
val = 12.6;
} else if (v < 0) {
continue;
} else {
val = 12.6 + (5 - v) * 0.1;
}
break;
case "hard":
const v2 = Math.floor(sh[key] / 2);
if (v2 === 9) {
val = 11.9;
} else if (v2 === 8) {
val = 12.1;
} else if (v2 === 7) {
val = 12.3;
} else if (v2 === 6) {
val = 12.5;
} else if (v2 === 5) {
val = 12.7;
} else if (v2 < 0) {
continue;
} else {
val = 12.7 + (5 - v2) * 0.1;
}
break;
case "exh":
const v3 = sh[key];
if (v3 >= 12 || v3 <= 0) {
continue;
}
val = 12.4 + (12 - v3) * 0.1;
break;
default:
throw new Error("??");
}
const stringVal = sh[`${key}_string` as const];
if (stringVal === "難易度未定") {
continue;
}
val = parseFloat(val.toFixed(2));
const ktKey = key === "exh" ? "kt-EXHC" : key === "hard" ? "kt-HC" : "kt-NC";
// If EXH, just save the string version as 12.xx
// Else, use the A/S+ thing that people are used to.
const text =
ktKey === "kt-EXHC"
? val.toFixed(2)
: `12${stringVal.replace(/(個人差|地力)/u, "")}`;
await db.charts.iidx.update(
{
chartID: chart.chartID,
},
{
$set: {
[`tierlistInfo.${ktKey}`]: {
text,
value: val,
individualDifference: stringVal.includes("個人差"),
},
},
}
);
logger.info(`Saved ${sh.title} value ${key} = ${val} (${text}).`);
}
}
logger.info("Done!");
process.exit(0);
}
async function HumanisedTitleLookup(originalTitle: string) {
let difficulty: Difficulties["iidx:SP"] = "ANOTHER";
let title = originalTitle;
if (title.match(/(†|†LEGGENDARIA)$/u)) {
difficulty = "LEGGENDARIA";
title = title.split(/(†|†LEGGENDARIA)$/u)[0];
} else if (title.match(/\[H\]$/u)) {
difficulty = "HYPER";
title = title.split("[")[0];
} else if (title.match(/\[A\]$/u)) {
difficulty = "ANOTHER"; // lmao
title = title.split("[")[0];
}
const song = await FindSongOnTitle("iidx", title);
if (!song) {
throw new Error(
`Could not resolve song ${title} (${difficulty}) (Original ${originalTitle}).`
);
}
const chart = await FindChartWithPTDF("iidx", song.id, "SP", difficulty);
if (!chart) {
throw new Error(
`Could not resolve chart ${title} ${difficulty} (Original ${originalTitle}).`
);
}
return chart;
}
FetchSP12Data();
+5
View File
@@ -0,0 +1,5 @@
import { InitSequenceDocs } from "external/mongo/sequence-docs";
if (require.main === module) {
InitSequenceDocs().then(() => process.exit(0));
}
@@ -0,0 +1,31 @@
import { Command } from "commander";
import db from "external/mongo/db";
import CreateLogCtx from "lib/logger/logger";
import { Random20Hex } from "utils/misc";
const logger = CreateLogCtx(__filename);
const program = new Command();
program.option("-c, --code <code>", "The code for this invite.");
program.parse(process.argv);
const options = program.opts();
if (!options.code) {
options.code = Random20Hex();
}
db.invites
.insert({
code: options.code,
createdBy: 1,
consumed: false,
createdAt: Date.now(),
consumedBy: null,
consumedAt: null,
})
.then(() => {
logger.info(`Created invite ${options.code}.`);
process.exit(0);
});
@@ -0,0 +1,21 @@
import deepmerge from "deepmerge";
import { ICollection } from "monk";
import { ChartDocument, IDStrings } from "tachi-common";
import { Random20Hex } from "utils/misc";
export async function QuickInsert<I extends IDStrings>(
base: Omit<ChartDocument<I>, "chartID">,
content: Partial<ChartDocument<I>>[],
coll: ICollection
) {
const docs = [];
for (const c of content) {
const doc = deepmerge(base, c);
doc.chartID = Random20Hex();
docs.push(doc);
}
await coll.insert(docs);
}
+16
View File
@@ -0,0 +1,16 @@
/* eslint-disable no-await-in-loop */
import { Command } from "commander";
import { SetIndexes } from "../external/mongo/indexes";
const program = new Command();
program.option("-d, --db <database>", "The database to index.");
program.option(
"-r, --reset",
"Whether to reset all indexes on this database before indexing or not."
);
program.parse(process.argv);
const options = program.opts();
SetIndexes(options.db, options.reset).then(() => process.exit(0));
@@ -0,0 +1,36 @@
import db from "external/mongo/db";
import CreateLogCtx from "lib/logger/logger";
const logger = CreateLogCtx(__filename);
async function DedupeScoreIDs() {
const dups = await db.scores.aggregate(
[
{
$group: {
_id: "$scoreID",
dups: { $addToSet: "$_id" },
count: { $sum: 1 },
},
},
{
$match: {
count: { $gt: 1 },
},
},
],
{ allowDiskUse: true }
);
logger.info(`Found ${dups.length} dups.`);
for (const dup of dups) {
dup.dups.shift();
await db.scores.remove({ _id: { $in: dup.dups } });
}
logger.info(`Done.`);
process.exit(0);
}
DedupeScoreIDs();
@@ -0,0 +1,5 @@
import { RecalcAllScores } from "utils/calculations/recalc-scores";
if (require.main === module) {
RecalcAllScores();
}
@@ -0,0 +1,43 @@
/* eslint-disable no-await-in-loop */
import db from "external/mongo/db";
import CreateLogCtx from "lib/logger/logger";
import { UpdateUsersGamePlaytypeStats } from "lib/score-import/framework/user-game-stats/update-ugs";
import { FormatUserDoc } from "utils/user";
const logger = CreateLogCtx(__filename);
export async function CreateGameProfiles() {
const users = await db.users.find({});
for (const user of users) {
const gpts = await db.scores.aggregate([
{
$match: {
userID: user.id,
},
},
{
$group: {
_id: {
game: "$game",
playtype: "$playtype",
},
},
},
]);
logger.info(`Found ${gpts.length} GPTs for ${FormatUserDoc(user)}`);
for (const gpt of gpts) {
const { game, playtype } = gpt._id;
logger.info(`Updating ${FormatUserDoc(user)}'s ${game} ${playtype} stats.`);
await UpdateUsersGamePlaytypeStats(game, playtype, user.id, null, logger);
}
}
logger.info(`Done.`);
}
if (require.main === module) {
CreateGameProfiles().then(() => process.exit(0));
}
@@ -0,0 +1,5 @@
import { RecalcSessions } from "utils/calculations/recalc-sessions";
if (require.main === module) {
RecalcSessions().then(() => process.exit(0));
}
@@ -0,0 +1,16 @@
import CreateLogCtx from "lib/logger/logger";
import { RecalcAllScores, UpdateAllPBs } from "utils/calculations/recalc-scores";
import { RecalcSessions } from "utils/calculations/recalc-sessions";
import { CreateGameProfiles } from "./recalc-game-profiles";
const logger = CreateLogCtx(__filename);
(async () => {
await RecalcAllScores();
await UpdateAllPBs();
await CreateGameProfiles();
await RecalcSessions();
logger.info(`Completely done!`);
process.exit(0);
})();
@@ -0,0 +1,6 @@
/* eslint-disable no-await-in-loop */
import { UpdateAllPBs } from "utils/calculations/recalc-scores";
if (require.main === module) {
UpdateAllPBs();
}
@@ -0,0 +1,284 @@
/* eslint-disable no-await-in-loop */
// This script syncs this tachi instances database up with the tachi-database-seeds.
import { execSync } from "child_process";
import { monkDB } from "external/mongo/db";
import fjsh from "fast-json-stable-hash";
import fs from "fs";
import CreateLogCtx, { KtLogger } from "lib/logger/logger";
import UpdateIsPrimaryStatus from "lib/score-mutation/update-isprimary";
import { Environment, TachiConfig } from "lib/setup/config";
import { BulkWriteOperation } from "mongodb";
import { ICollection } from "monk";
import os from "os";
import path from "path";
import {
BMSCourseDocument,
ChartDocument,
FolderDocument,
SongDocument,
TableDocument,
} from "tachi-common";
import { RecalcAllScores } from "utils/calculations/recalc-scores";
import { InitaliseFolderChartLookup } from "utils/folder";
interface SyncInstructions {
pattern: RegExp;
handler: (c: any[], collection: ICollection<any>, logger: KtLogger) => Promise<unknown>;
}
async function RemoveNotPresent<T>(
documents: T[],
collection: ICollection<T>,
field: keyof T,
logger: KtLogger
) {
logger.verbose(`Removing all documents that are no longer present.`);
// Remove anything no longer present.
// Note that $nin is incredibly slow.
// @ts-expect-error generic system failing
const r = await collection.remove({
[field]: { $nin: documents.map((e) => e[field]) },
});
// @ts-expect-error These types are broken!
if (r.deletedCount) {
// @ts-expect-error These types are broken!
logger.info(`Removed ${r.deletedCount} documents.`);
}
}
async function GenericUpsert<T>(
documents: T[],
collection: ICollection<T>,
field: keyof T,
logger: KtLogger,
remove = false
) {
logger.verbose(`Running bulkwrite.`);
const bwriteOps: BulkWriteOperation<T>[] = [];
const allExistingDocs = await collection.find({});
const map = new Map();
for (const doc of allExistingDocs) {
map.set(doc[field], doc);
}
const changedFields = [];
let i = 0;
for (const document of documents) {
i++;
if (i % 10_000 === 0) {
logger.verbose(`On document ${i}/${documents.length}.`);
}
const exists = map.get(document[field]);
if (!exists) {
bwriteOps.push({
// @ts-expect-error Actually, T is assignable to OptionalId<T>.
insertOne: { document },
});
} else if (fjsh.hash(document, "sha256") !== fjsh.hash(exists, "sha256")) {
bwriteOps.push({
replaceOne: {
// @ts-expect-error Known X->Y generic issue.
filter: {
[field]: document[field],
},
replacement: document,
},
});
changedFields.push(field);
}
// free some memory.
map.delete(document[field]);
}
if (bwriteOps.length === 0) {
logger.verbose(`No differences. Not performing any update.`);
} else {
const { deletedCount, insertedCount, matchedCount, upsertedCount, modifiedCount } =
await collection.bulkWrite(bwriteOps);
logger.info(`Performed bulkWrite.`, {
deletedCount,
insertedCount,
matchedCount,
upsertedCount,
modifiedCount,
});
}
if (remove) {
await RemoveNotPresent(documents, collection, field, logger);
}
return {
thingsChanged: bwriteOps.length,
changedFields,
};
}
const syncInstructions: SyncInstructions[] = [
{
pattern: /^charts-(usc|bms)$/u,
handler: async (
charts: ChartDocument[],
collection: ICollection<ChartDocument>,
logger
) => {
// Since the USC and BMS databases are managed Bokutachi-side, we
// shouldn't be honoring any sort of updates from tachi-database-seeds
// aside from an initial one.
//
// However, in practice, these syncs are the best way to actually update
// issues in the database.
// We're going to disable this anyway.
// const isInitial = (await collection.findOne()) === null;
const r = await GenericUpsert(charts, collection, "chartID", logger, false);
if (r.thingsChanged) {
await InitaliseFolderChartLookup();
await UpdateIsPrimaryStatus();
}
},
},
{
pattern: /^charts-/u,
handler: async (
charts: ChartDocument[],
collection: ICollection<ChartDocument>,
logger
) => {
const r = await GenericUpsert(charts, collection, "chartID", logger, true);
if (r.thingsChanged) {
await InitaliseFolderChartLookup();
await UpdateIsPrimaryStatus();
await RecalcAllScores({
chartID: { $in: r.changedFields },
});
}
},
},
{
pattern: /^songs-/u,
handler: async (songs: SongDocument[], collection: ICollection<SongDocument>, logger) => {
const r = await GenericUpsert(songs, collection, "id", logger);
if (r.thingsChanged) {
await RecalcAllScores({
songID: { $in: r.changedFields },
});
}
},
},
{
pattern: /^folders$/u,
handler: async (
folders: FolderDocument[],
collection: ICollection<FolderDocument>,
logger
) => {
const r = await GenericUpsert(folders, collection, "folderID", logger, true);
if (r) {
await InitaliseFolderChartLookup();
}
},
},
{
pattern: /^tables$/u,
handler: (tables: TableDocument[], collection: ICollection<TableDocument>, logger) =>
GenericUpsert(tables, collection, "tableID", logger, true),
},
{
pattern: /^bms-course-lookup$/u,
handler: (
bmsCourseDocuments: BMSCourseDocument[],
collection: ICollection<BMSCourseDocument>,
logger
) => GenericUpsert(bmsCourseDocuments, collection, "md5sums", logger),
},
];
const logger = CreateLogCtx("Database Sync");
async function SynchroniseDBWithSeeds() {
const seedsDir = fs.mkdtempSync(path.join(os.tmpdir(), "tachi-database-seeds-"));
logger.info(`Cloning data to ${seedsDir}.`);
fs.rmSync(seedsDir, { recursive: true, force: true });
// Wait for mongo to connect first.
await monkDB.then(() => void 0);
execSync(
`git clone https://github.com/TNG-dev/tachi-database-seeds -b "${
Environment.nodeEnv === "production" ? "master" : "develop"
}" --depth=1 "${seedsDir}"`,
{
stdio: "inherit",
}
);
const collections = fs.readdirSync(path.join(seedsDir, "collections"));
for (const jsonName of collections) {
const collectionName = path.parse(jsonName).name;
const spawnLogger = CreateLogCtx(`${collectionName} Sync`);
if (collectionName.startsWith("songs-") || collectionName.startsWith("charts-")) {
const game = collectionName.split("-")[1];
if (!TachiConfig.GAMES.includes(game as any)) {
spawnLogger.verbose(
`Skipping ${collectionName} (${game}) as it isn't for ${TachiConfig.NAME}.`
);
continue;
}
}
spawnLogger.verbose(`Getting data.`);
let data = JSON.parse(
fs.readFileSync(path.join(seedsDir, "collections", jsonName), "utf-8")
);
spawnLogger.verbose(`Found ${data.length} documents.`);
let matchedSomething = false;
for (const syncInst of syncInstructions) {
if (collectionName.match(syncInst.pattern)) {
spawnLogger.verbose(`Starting handler...`);
await syncInst.handler(data, monkDB.get(collectionName), spawnLogger);
matchedSomething = true;
break;
}
}
// free memory forcefully
data = null;
if (!matchedSomething) {
spawnLogger.warn(
`Collection ${collectionName} didn't match any sync instructions. Skipping.`
);
}
}
logger.info(`Done.`);
}
if (require.main === module) {
SynchroniseDBWithSeeds().then(() => process.exit(0));
}
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "../tsconfig.json",
"include": [
"./**/*.ts",
],
}
+59
View File
@@ -0,0 +1,59 @@
// note: a hell of a lot of this code is copy pasted
// this is just a script, so, feel free to refactor it.
import { Command } from "commander";
import { Databases, monkDB } from "external/mongo/db";
import { DatabaseSchemas } from "external/mongo/schemas";
import CreateLogCtx from "lib/logger/logger";
import { PrudenceError } from "prudence";
import { FormatPrError } from "utils/prudence";
const program = new Command();
program.option("-c, --collection <collectionName>");
program.parse(process.argv);
const options = program.opts();
const logger = CreateLogCtx(__filename);
export async function ValidateCollection(collectionName: Databases): Promise<void> {
const schemaRunner = DatabaseSchemas[collectionName];
const documents = await monkDB.get(collectionName).count({});
logger.info(`Validating ${collectionName}. This is ${documents} documents.
`);
let success = 0;
let fails = 0;
await monkDB
.get(collectionName)
.find({})
// @ts-expect-error faulty monk types
.each((c) => {
try {
schemaRunner(c);
success++;
} catch (err) {
logger.error(`[${collectionName}]: ${FormatPrError(err as PrudenceError)}`, c);
fails++;
}
});
logger.info(`Success: ${success} (${(100 * success) / documents}%)`);
logger.info(`Failures: ${fails} (${(100 * fails) / documents}%)`);
}
export async function ValidateAllCollections() {
for (const collectionName in DatabaseSchemas) {
// eslint-disable-next-line no-await-in-loop
await ValidateCollection(collectionName as keyof typeof DatabaseSchemas);
}
}
if (require.main === module) {
if (options.collection) {
ValidateCollection(options.collection).then(() => process.exit(0));
} else {
ValidateAllCollections().then(() => process.exit(0));
}
}