feat: start actionising on the west coast

This commit is contained in:
zk
2026-03-19 20:20:11 +00:00
parent bfe5fad78f
commit c80e2ea2ef
695 changed files with 6132 additions and 49249 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"license": "MIT",
"scripts": {
"test": "vitest run",
"test": "vitest",
"test:watch": "vitest",
"typecheck": "tsc --noEmit"
},
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe } from "vitest";
import { api } from "./setup"
import { api } from "./setup";
import { it } from "vitest";
describe("GET /activity", () => {
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe } from "vitest";
import { api } from "./setup"
import { api } from "./setup";
import { it } from "vitest";
describe("GET /config/game-support", () => {
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe } from "vitest";
import { api } from "./setup"
import { api } from "./setup";
import { it } from "vitest";
describe("GET /games", () => {
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe } from "vitest";
import { api } from "./setup"
import { api } from "./setup";
import { it } from "vitest";
describe("GET /imports/:importID", () => {
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe } from "vitest";
import { api } from "./setup"
import { api } from "./setup";
import { it } from "vitest";
describe("GET /scores/:scoreID", () => {
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe } from "vitest";
import { api } from "./setup"
import { api } from "./setup";
import { it } from "vitest";
describe("GET /search", () => {
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe } from "vitest";
import { api } from "./setup"
import { api } from "./setup";
import { it } from "vitest";
describe("GET /sessions/:sessionID", () => {
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe } from "vitest";
import { api } from "./setup"
import { api } from "./setup";
import { it } from "vitest";
describe("GET /status", () => {
+1 -1
View File
@@ -1,5 +1,5 @@
import { describe } from "vitest";
import { api } from "./setup"
import { api } from "./setup";
import { it } from "vitest";
describe("GET /users", () => {
+9
View File
@@ -0,0 +1,9 @@
import configTachi from "eslint-config-tachi";
export default [
...configTachi.base,
configTachi.node,
{
ignores: ["js/**", "node_modules/**"],
},
];
+1 -1
View File
@@ -36,7 +36,7 @@
"dependencies": {
"pino": "catalog:",
"pino-pretty": "catalog:",
"zod": "^4.3.6"
"zod": "catalog:"
},
"devDependencies": {
"@typescript/native-preview": "catalog:",
+4 -4
View File
@@ -1,7 +1,7 @@
import { AppendLogCtx, log as baseLogger } from "#log.js";
import z, { ZodObject } from "zod";
import { type ZodObject } from "zod";
export type ActionResult = "GOOD" | "BAD" | "THROW";
export type ActionResult = "BAD" | "GOOD" | "THROW";
/**
* Secret input and output fields (keys starting with "!") are stripped before logging.
@@ -67,10 +67,10 @@ export function MakeActionGuts({
kind,
fn: actionBodyFn,
}: {
db: any;
appName: string;
kind: string;
db: any;
fn: (taker: ActionTaker | AnonActionTaker, input: object) => Promise<object>;
kind: string;
}): unknown {
return async (taker: ActionTaker | AnonActionTaker, input: Record<string, unknown>) => {
const ts_start = new Date();
+1 -1
View File
@@ -1,4 +1,3 @@
export * from "./log";
export {
AcctInfo,
ActionResult,
@@ -7,3 +6,4 @@ export {
ExpectedErr,
MakeActionGuts,
} from "./actions";
export * from "./log";
+41 -86
View File
@@ -1,5 +1,6 @@
import type { GameGroup, integer, TachiServerCoreConfig } from "tachi-common";
import { log } from "bliss/log";
import { config } from "dotenv";
import { p } from "prudence";
@@ -9,29 +10,6 @@ import { FormatPrError } from "./utils/prudence";
// Initialise .env.
config();
// the real log tries to bind to discord, and is dependent on the options
// below.
const log = console;
export interface BotConfig {
TACHI_SERVER_LOCATION: string;
HTTP_SERVER: {
URL: string;
};
OAUTH: {
CLIENT_ID: string;
CLIENT_SECRET: string;
};
DISCORD: {
ADMIN_USERS: Array<string>;
APPROVED_ROLE?: string;
GAME_CHANNELS: Partial<Record<GameGroup, string>>;
LIMBO_CHANNEL?: string;
SERVER_ID: string;
TOKEN: string;
};
}
function ParseGameChannels(raw: string | undefined): Partial<Record<GameGroup, string>> {
if (!raw) {
return {};
@@ -62,57 +40,17 @@ function ParseGameChannels(raw: string | undefined): Partial<Record<GameGroup, s
return parsed as Partial<Record<GameGroup, string>>;
}
function ParseBotConfig(): BotConfig {
const err = p(
process.env,
{
TACHI_SERVER_LOCATION: "string",
HTTP_SERVER_URL: "string",
OAUTH_CLIENT_ID: "string",
OAUTH_CLIENT_SECRET: "string",
DISCORD_TOKEN: "string",
DISCORD_SERVER_ID: "string",
DISCORD_GAME_CHANNELS: "*string",
DISCORD_ADMIN_USERS: "*string",
DISCORD_APPROVED_ROLE: "*string",
DISCORD_LIMBO_CHANNEL: "*string",
},
{},
{ allowExcessKeys: true },
);
if (err) {
log.error(FormatPrError(err, "Invalid environment. Cannot safely boot."));
throw err;
}
const gameChannels = ParseGameChannels(process.env.DISCORD_GAME_CHANNELS);
const adminUsers = process.env.DISCORD_ADMIN_USERS
? process.env.DISCORD_ADMIN_USERS.split(",").filter(Boolean)
: [];
return {
TACHI_SERVER_LOCATION: process.env.TACHI_SERVER_LOCATION!,
HTTP_SERVER: {
URL: process.env.HTTP_SERVER_URL!,
},
OAUTH: {
CLIENT_ID: process.env.OAUTH_CLIENT_ID!,
CLIENT_SECRET: process.env.OAUTH_CLIENT_SECRET!,
},
DISCORD: {
TOKEN: process.env.DISCORD_TOKEN!,
SERVER_ID: process.env.DISCORD_SERVER_ID!,
GAME_CHANNELS: gameChannels,
ADMIN_USERS: adminUsers,
APPROVED_ROLE: process.env.DISCORD_APPROVED_ROLE,
LIMBO_CHANNEL: process.env.DISCORD_LIMBO_CHANNEL,
},
};
}
export interface ProcessEnvironment {
TACHI_SERVER_LOCATION: string;
HTTP_SERVER_URL: string;
OAUTH_CLIENT_ID: string;
OAUTH_CLIENT_SECRET: string;
DISCORD_TOKEN: string;
DISCORD_SERVER_ID: string;
DISCORD_GAME_CHANNELS: Partial<Record<GameGroup, string>>;
DISCORD_ADMIN_USERS: string[];
DISCORD_APPROVED_ROLE: string | undefined;
DISCORD_LIMBO_CHANNEL: string | undefined;
NODE_ENV: "dev" | "production" | "staging" | "test";
POSTGRES_URL: string;
PORT: integer;
@@ -132,6 +70,17 @@ function ParseEnvVars() {
PORT: (self) =>
p.isPositiveInteger(Number(self)) === true ||
"Should be a string representing a whole integer port.",
TACHI_SERVER_LOCATION: "string",
HTTP_SERVER_URL: "string",
OAUTH_CLIENT_ID: "string",
OAUTH_CLIENT_SECRET: "string",
DISCORD_TOKEN: "string",
DISCORD_SERVER_ID: "string",
DISCORD_GAME_CHANNELS: "*string",
DISCORD_ADMIN_USERS: "*string",
DISCORD_APPROVED_ROLE: "*string",
DISCORD_LIMBO_CHANNEL: "*string",
},
{},
{ allowExcessKeys: true },
@@ -143,14 +92,24 @@ function ParseEnvVars() {
throw err;
}
return {
NODE_ENV: process.env.NODE_ENV,
POSTGRES_URL: process.env.POSTGRES_URL,
const Env: ProcessEnvironment = {
NODE_ENV: process.env.NODE_ENV as "dev" | "production" | "staging" | "test",
POSTGRES_URL: process.env.POSTGRES_URL!,
PORT: Number(process.env.PORT),
} as ProcessEnvironment;
}
TACHI_SERVER_LOCATION: process.env.TACHI_SERVER_LOCATION!,
HTTP_SERVER_URL: process.env.HTTP_SERVER_URL!,
OAUTH_CLIENT_ID: process.env.OAUTH_CLIENT_ID!,
OAUTH_CLIENT_SECRET: process.env.OAUTH_CLIENT_SECRET!,
DISCORD_TOKEN: process.env.DISCORD_TOKEN!,
DISCORD_SERVER_ID: process.env.DISCORD_SERVER_ID!,
DISCORD_GAME_CHANNELS: ParseGameChannels(process.env.DISCORD_GAME_CHANNELS),
DISCORD_ADMIN_USERS: process.env.DISCORD_ADMIN_USERS?.split(",") ?? [],
DISCORD_APPROVED_ROLE: process.env.DISCORD_APPROVED_ROLE,
DISCORD_LIMBO_CHANNEL: process.env.DISCORD_LIMBO_CHANNEL,
};
export const BotConfig: BotConfig = ParseBotConfig();
return Env;
}
// The Tachi Server exports all of the information about it. This saves us having to
// sync more metadata across instances.
@@ -160,14 +119,10 @@ async function GetServerConfig() {
// This *should* be solved with top-level-await, but good luck actually getting
// typescript to output the right stuff here.
const res = await fetch(`${BotConfig.TACHI_SERVER_LOCATION}/api/v1/config`).then((res) =>
res.json(),
);
const res = await fetch(`${Env.TACHI_SERVER_LOCATION}/api/v1/config`).then((res) => res.json());
if (!res.success) {
log.error(
`Failed to fetch server info from ${BotConfig.TACHI_SERVER_LOCATION}. Can't run.`,
);
log.error(`Failed to fetch server info from ${Env.TACHI_SERVER_LOCATION}. Can't run.`);
process.exit(1);
}
@@ -181,7 +136,7 @@ export const Env = ParseEnvVars();
// General warnings for config misuse.
// This warns people if their parent server supports games that they aren't acknowledging.
for (const game of ServerConfig.GAMES) {
if (!Object.prototype.hasOwnProperty.call(BotConfig.DISCORD.GAME_CHANNELS, game)) {
if (!Object.prototype.hasOwnProperty.call(Env.DISCORD_GAME_CHANNELS, game)) {
log.warn(
`${ServerConfig.NAME} declares support for ${game}, but no channel is mapped to it. Set DISCORD_GAME_CHANNELS to include it.`,
);
+4 -4
View File
@@ -2,7 +2,7 @@ import { log } from "#utils/log.js";
import { GetLimboChannel } from "#utils/misc";
import { Client, type CommandInteraction, Intents, type SelectMenuInteraction } from "discord.js";
import { BotConfig, Env, ServerConfig } from "./config";
import { Env, ServerConfig } from "./config";
import { handleIsCommand } from "./interaction-handlers/handle-is-command";
import { GetUserAndTokenForDiscordID } from "./query/user-map";
import { app } from "./server/server";
@@ -25,7 +25,7 @@ export const client = new Client({
});
client.on("guildMemberAdd", async (_member) => {
if (BotConfig.DISCORD.APPROVED_ROLE && BotConfig.DISCORD.LIMBO_CHANNEL) {
if (Env.DISCORD_APPROVED_ROLE && Env.DISCORD_LIMBO_CHANNEL) {
const channel = GetLimboChannel(client);
await channel.send(
@@ -62,7 +62,7 @@ client.on("interactionCreate", async (interaction) => {
* If a user tries to do anything without auth, Tell them to authenticate.
*/
async function RequireUserAuth(interaction: CommandInteraction | SelectMenuInteraction) {
const oAuthLink = `${BotConfig.TACHI_SERVER_LOCATION}/oauth/request-auth?clientID=${BotConfig.OAUTH.CLIENT_ID}&context=${interaction.user.id}`;
const oAuthLink = `${Env.TACHI_SERVER_LOCATION}/oauth/request-auth?clientID=${Env.OAUTH_CLIENT_ID}&context=${interaction.user.id}`;
const dmChannel = await interaction.user.createDM();
@@ -79,7 +79,7 @@ void (async () => {
log.info(`Booting Tachi Bot ${VERSION_PRETTY}.`);
// Login to discord.
await client.login(BotConfig.DISCORD.TOKEN);
await client.login(Env.DISCORD_TOKEN);
log.info(`Logged in successfully to ${client.guilds.cache.size} guilds.`);
+2 -2
View File
@@ -2,7 +2,7 @@ import type { RequestHandler } from "express";
import { log } from "#utils/log";
import { BotConfig } from "../config";
import { Env } from "../config";
/**
* Middleware that checks that a webhook request has Authorization set to
@@ -32,7 +32,7 @@ export const ValidateWebhookRequest: RequestHandler = (req, res, next) => {
});
}
if (value !== BotConfig.OAUTH.CLIENT_SECRET) {
if (value !== Env.OAUTH_CLIENT_SECRET) {
log.warn(`Recieved invalid auth value from ${req.ip}. Has the client secret been changed?`);
return res.status(403).json({
success: false,
+4 -4
View File
@@ -6,7 +6,7 @@ import express, { type Express } from "express";
import path from "path";
import { ACTION_Register } from "../actions/register";
import { BotConfig, Env } from "../config";
import { Env } from "../config";
import { RequestTypes, TachiServerV1Get, TachiServerV1Request } from "../utils/fetch-tachi";
import { VERSION_PRETTY } from "../version";
import { HandleClassUpdateV1 } from "../webhook-handlers/class-update";
@@ -64,10 +64,10 @@ app.get("/oauth/callback", async (req, res) => {
null,
{
code: req.query.code,
client_id: BotConfig.OAUTH.CLIENT_ID,
client_secret: BotConfig.OAUTH.CLIENT_SECRET,
client_id: Env.OAUTH_CLIENT_ID,
client_secret: Env.OAUTH_CLIENT_SECRET,
grant_type: "authorization_code",
redirect_uri: `${BotConfig.HTTP_SERVER.URL}/oauth/callback`,
redirect_uri: `${Env.HTTP_SERVER_URL}/oauth/callback`,
},
);
@@ -1,6 +1,6 @@
import type { SlashCommand } from "./types";
import { BotConfig, ServerConfig } from "../config";
import { Env, ServerConfig } from "../config";
import faq from "./commands/faq";
import invite from "./commands/invite";
import letmein from "./commands/letmein";
@@ -16,7 +16,7 @@ export const SLASH_COMMANDS: Map<string, SlashCommand> = new Map(
}),
);
if (BotConfig.DISCORD.APPROVED_ROLE) {
if (Env.DISCORD_APPROVED_ROLE) {
SLASH_COMMANDS.set("letmein", letmein);
}
@@ -3,7 +3,7 @@ import deepmerge from "deepmerge";
import type { SlashCommand } from "../types";
import { BotConfig, ServerConfig } from "../../config";
import { Env, ServerConfig } from "../../config";
import { CreateEmbed } from "../../utils/embeds";
const NEUTRAL_FAQ_ENTRIES: Record<string, string> = {
@@ -117,12 +117,12 @@ const KAMAI_FAQ_ENTRIES: Record<string, string> = {
})`,
sdvx_1259: `Automation Paradise is song ID 1259. It is not a real song, and it is not supported by ${ServerConfig.NAME}. Related autopara-only songs are not supported, either.`,
invites: `To invite your friends, go to your profile and select 'Invites'. From there, you can create an invite code.
Your friend can then go to ${BotConfig.TACHI_SERVER_LOCATION} and sign up with the code! You can also invite them to the discord.`,
Your friend can then go to ${Env.TACHI_SERVER_LOCATION} and sign up with the code! You can also invite them to the discord.`,
};
const BOKU_FAQ_ENTRIES: Record<string, string> = {
usc_hard_mode: `Hard Mode windows are not supported on ${ServerConfig.NAME}. Your scores **will be ignored** if they are played on non-standard windows.`,
ir_login: `You **must** put your API Key in the password field for the Bokutachi IR, **NOT your real password!**. See instructions here: ${BotConfig.TACHI_SERVER_LOCATION}/import/beatoraja-ir.
ir_login: `You **must** put your API Key in the password field for the Bokutachi IR, **NOT your real password!**. See instructions here: ${Env.TACHI_SERVER_LOCATION}/import/beatoraja-ir.
(This is because putting your real password in there is a security nightmare.)`,
lr2: `Support for LR2 (the LR2Hook) are **not officially maintained** and therefore **not managed by bokutachi developers**.
We urge you to use a more modern BMS client like lr2oraja, unless you have very good reason to stay on LR2.
@@ -1,4 +1,4 @@
import { BotConfig } from "#config";
import { Env } from "#config";
import { SlashCommandBuilder } from "@discordjs/builders";
import { ExpectedErr } from "bliss";
import { GuildMember } from "discord.js";
@@ -13,7 +13,7 @@ const command: SlashCommand = {
.setDescription("Let yourself into the server.")
.toJSON(),
exec: async (interaction) => {
if (!BotConfig.DISCORD.APPROVED_ROLE) {
if (!Env.DISCORD_APPROVED_ROLE) {
return null; // no-op
}
@@ -26,7 +26,7 @@ const command: SlashCommand = {
{ ip: null },
{
discord_user_id: interaction.user.id,
role_id: BotConfig.DISCORD.APPROVED_ROLE,
role_id: Env.DISCORD_APPROVED_ROLE,
"!member": interaction.member,
},
);
@@ -1,4 +1,4 @@
import { BotConfig, ServerConfig } from "#config";
import { Env, ServerConfig } from "#config";
import { SlashCommandBuilder } from "@discordjs/builders";
import { ExpectedErr } from "bliss";
@@ -66,7 +66,7 @@ const command: SlashCommand = {
.addField("Errors", result.error_count.toString(), true)
.addField(
"Your Profile",
`${BotConfig.TACHI_SERVER_LOCATION}/u/${result.user_id}/games/${result.game}`,
`${Env.TACHI_SERVER_LOCATION}/u/${result.user_id}/games/${result.game}`,
);
} catch (e) {
if (ExpectedErr.is(e)) {
@@ -4,12 +4,12 @@ import { log } from "#utils/log";
import { REST } from "@discordjs/rest";
import { Routes } from "discord-api-types/v9";
import { BotConfig, Env } from "../config";
import { Env } from "../config";
import { SLASH_COMMANDS } from "./commands";
const rest = new REST({
version: "9",
}).setToken(BotConfig.DISCORD.TOKEN);
}).setToken(Env.DISCORD_TOKEN);
/**
* Register our slash commands. If in prod, these
@@ -34,10 +34,7 @@ export async function RegisterSlashCommands(client: Client): Promise<void> {
log.info("Registering guild slash commands.");
await rest.put(
Routes.applicationGuildCommands(
client.application!.id,
BotConfig.DISCORD.SERVER_ID,
),
Routes.applicationGuildCommands(client.application!.id, Env.DISCORD_SERVER_ID),
{
body: commandsArray.map((command) => command.info),
},
+1 -1
View File
@@ -23,7 +23,7 @@ export async function createTestAccount(
): Promise<TestAccount> {
const { id } = await db
.insertInto("account")
.values({ username })
.values({ username, about: "Test user for tests." })
.returning("id")
.executeTakeFirstOrThrow();
+2 -2
View File
@@ -12,7 +12,7 @@ import type {
UserDocument,
} from "tachi-common";
import { BotConfig } from "#config";
import { Env } from "#config";
import { log } from "#utils/log";
import type { ImportDeferred, ImportPollStatus, UGPTStats } from "./return-types";
@@ -161,7 +161,7 @@ export async function PerformScoreImport(
throw new Error(`Failed to import scores.
Your authentication with this service has expired, and a bug on their end prevents us from automatically renewing it.
Please go to ${BotConfig.TACHI_SERVER_LOCATION}/u/me/integrations/services to un-link and re-link.`);
Please go to ${Env.TACHI_SERVER_LOCATION}/u/me/integrations/services to un-link and re-link.`);
}
throw new Error(`Failed to import scores. ${pollRes.description}.`);
+3 -3
View File
@@ -2,7 +2,7 @@ import type { ImportDocument, integer, UserDocument } from "tachi-common";
import { MessageEmbed } from "discord.js";
import { BotConfig, ServerConfig } from "../config";
import { Env, ServerConfig } from "../config";
import { PrependTachiUrl } from "./fetch-tachi";
import { FormatDate, Pluralise } from "./misc";
@@ -30,7 +30,7 @@ export function CreateImportEmbed(importDoc: ImportDocument) {
.addField("Errors", importDoc.errors.length.toString(), true)
.addField(
"Your Profile",
`${BotConfig.TACHI_SERVER_LOCATION}/u/${importDoc.userID}/games/${importDoc.game}`,
`${Env.TACHI_SERVER_LOCATION}/u/${importDoc.userID}/games/${importDoc.game}`,
);
}
@@ -40,5 +40,5 @@ export function CreateUserEmbed(userDoc: UserDocument) {
.setThumbnail(PrependTachiUrl(`/users/${userDoc.id}/pfp`))
.setDescription(userDoc.status ?? "No status...")
.addField("Join Date", FormatDate(userDoc.joinDate))
.setURL(`${BotConfig.TACHI_SERVER_LOCATION}/u/${userDoc.username}`);
.setURL(`${Env.TACHI_SERVER_LOCATION}/u/${userDoc.username}`);
}
+2 -2
View File
@@ -4,7 +4,7 @@ import { log } from "#utils/log";
import fetch from "node-fetch";
import { URLSearchParams } from "url";
import { BotConfig } from "../config";
import { Env } from "../config";
import { VERSION_STR } from "../version";
type ApiResponseBase = {
@@ -129,7 +129,7 @@ export function PrependTachiUrl(url: string, version: "1" = "1"): string {
url = `/${url}`;
}
return `${BotConfig.TACHI_SERVER_LOCATION}/api/v${version}${url}`;
return `${Env.TACHI_SERVER_LOCATION}/api/v${version}${url}`;
}
/**
+5 -5
View File
@@ -15,7 +15,7 @@ import {
type Playtype,
} from "tachi-common";
import { BotConfig } from "../config";
import { Env } from "../config";
export function Sleep(ms: number) {
return new Promise<void>((resolve) => {
@@ -99,7 +99,7 @@ export function FormatClass(
* Given a game, return the discord channel it's associated with.
*/
export function GetGameChannel(client: Client, game: GameGroup) {
const gameChannelID = BotConfig.DISCORD.GAME_CHANNELS[game];
const gameChannelID = Env.DISCORD_GAME_CHANNELS[game];
if (!gameChannelID) {
throw new Error(
@@ -123,7 +123,7 @@ export function GetGameChannel(client: Client, game: GameGroup) {
}
export function GetLimboChannel(client: Client) {
const limboCID = BotConfig.DISCORD.LIMBO_CHANNEL;
const limboCID = Env.DISCORD_LIMBO_CHANNEL;
if (!limboCID) {
throw new Error(`Attempted to get channel for #limbo, but none was registered.`);
@@ -147,12 +147,12 @@ export function GetLimboChannel(client: Client) {
*/
export function CreateChartLink(chart: ChartDocument, game: GameGroup) {
if (chart.isPrimary) {
return `${BotConfig.TACHI_SERVER_LOCATION}/games/${game}/${chart.playtype}/songs/${
return `${Env.TACHI_SERVER_LOCATION}/games/${game}/${chart.playtype}/songs/${
chart.songID
}/${encodeURIComponent(chart.difficulty)}`;
}
return `${BotConfig.TACHI_SERVER_LOCATION}/games/${game}/${chart.playtype}/songs/${chart.songID}/${chart.chartID}`;
return `${Env.TACHI_SERVER_LOCATION}/games/${game}/${chart.playtype}/songs/${chart.songID}/${chart.chartID}`;
}
export function ConvertInputIntoGenerousRegex(input: string) {
@@ -10,7 +10,7 @@ import {
type WebhookEventClassUpdateV1,
} from "tachi-common";
import { BotConfig } from "../config";
import { Env } from "../config";
import { client } from "../main";
import { GetUGPTStats, GetUserInfo } from "../utils/api-requests";
import { CreateEmbed } from "../utils/embeds";
@@ -62,9 +62,7 @@ export async function HandleClassUpdateV1(
.setTitle(
`${userDoc.username} just achieved ${newClass} in ${FormatGameGroup(game, playtype)}!`,
)
.setURL(
`${BotConfig.TACHI_SERVER_LOCATION}/users/${userDoc.username}/games/${game}/${playtype}`,
)
.setURL(`${Env.TACHI_SERVER_LOCATION}/users/${userDoc.username}/games/${game}/${playtype}`)
.setThumbnail(PrependTachiUrl(`/users/${userDoc.id}/pfp`));
if (event.old !== null) {
@@ -1,4 +1,4 @@
import { BotConfig } from "#config";
import { Env } from "#config";
import { PrependTachiUrl } from "#utils/fetch-tachi.js";
import { log } from "#utils/log";
import {
@@ -53,7 +53,7 @@ export async function HandleGoalAchievedV1(
)}!`,
)
.setThumbnail(PrependTachiUrl(`/users/${userDoc.id}/pfp`))
.setURL(`${BotConfig.TACHI_SERVER_LOCATION}/u/${userDoc.username}`)
.setURL(`${Env.TACHI_SERVER_LOCATION}/u/${userDoc.username}`)
.addFields(
event.goals.map((e) => {
const goal = goalMap.get(e.goalID)!;
@@ -1,4 +1,4 @@
import { BotConfig } from "#config";
import { Env } from "#config";
import { PrependTachiUrl } from "#utils/fetch-tachi.js";
import { log } from "#utils/log";
import { GetGameGroupConfig, type integer, type WebhookEventQuestAchievedV1 } from "tachi-common";
@@ -33,7 +33,7 @@ export async function HandleQuestAchievedV1(
const embed = CreateEmbed(userDoc.id)
.setThumbnail(PrependTachiUrl(`/users/${userDoc.id}/pfp`))
.setURL(`${BotConfig.TACHI_SERVER_LOCATION}/u/${userDoc.username}`)
.setURL(`${Env.TACHI_SERVER_LOCATION}/u/${userDoc.username}`)
.setTitle(
`${userDoc.username} just completed the ${quest.name}${shouldShowPlaytype} quest!`,
);
+5 -4
View File
@@ -16,10 +16,11 @@ export default defineConfig({
css: {
preprocessorOptions: {
scss: {
additionalData:
process.env.VITE_TCHIC_MODE === "boku"
? "$primary: #527acc;"
: "$primary: #e61c6e;",
additionalData:
process.env.VITE_TCHIC_MODE === "boku"
? "$primary: #527acc;"
: "$primary: #e61c6e;",
silenceDeprecations: ["import", "global-builtin", "color-functions", "if-function"],
},
},
},
+1 -3
View File
@@ -12,7 +12,7 @@
}
},
"scripts": {
"test": "tap",
"test": "vitest",
"build": "tsgo -p tsconfig.build.json",
"prepublishOnly": "tsgo -p tsconfig.build.json",
"lint": "eslint .",
@@ -43,9 +43,7 @@
},
"devDependencies": {
"@typescript/native-preview": "catalog:",
"@types/tap": "catalog:",
"eslint-config-tachi": "workspace:*",
"tap": "workspace:*",
"typescript": "catalog:"
},
"nyc": {
+1
View File
@@ -8,6 +8,7 @@ export * from "./constants/grade-boundaries";
export * as StaticConfig from "./constants/import-types";
export * from "./constants/permissions";
export * as Schemas from "./lib/schemas";
export * from "./lib/zod-schemas";
export * from "./seeds/types";
export * from "./types";
export * from "./utils/util";
+4
View File
@@ -0,0 +1,4 @@
import { ALL_PERMISSIONS } from "#constants/permissions.js";
import z from "zod";
export const zodPermission = z.enum(Object.keys(ALL_PERMISSIONS));
+5 -5
View File
@@ -15,11 +15,11 @@ import { applyMigrations, getMigrationInfo, revertLastMigration } from "tachi-db
const DEFAULT_MIGRATIONS_DIR = "/tachi/db/migrations";
function getConnectionString(opts: { databaseUrl?: string }): string {
const url = opts.databaseUrl ?? process.env.DATABASE_URL;
const url = opts.databaseUrl ?? process.env.POSTGRES_URL;
if (!url) {
console.error(
"[migrate] No database URL provided. Set DATABASE_URL, or pass --database-url.",
"[migrate] No database URL provided. Set POSTGRES_URL, or pass --database-url.",
);
process.exit(1);
}
@@ -88,7 +88,7 @@ const program = new Command();
program
.name("tachi-migrate")
.description("Database migration CLI, modelled after sqlx migrate.")
.option("--database-url <url>", "Postgres connection string (overrides DATABASE_URL env var)");
.option("--database-url <url>", "Postgres connection string (overrides POSTGRES_URL env var)");
// ---------------------------------------------------------------------------
// database subcommands
@@ -98,7 +98,7 @@ const databaseCmd = program.command("database").description("Create or drop the
databaseCmd
.command("create")
.description("Create the database specified by DATABASE_URL.")
.description("Create the database specified by POSTGRES_URL.")
.action(async () => {
const parentOpts = program.opts<{ databaseUrl?: string }>();
const connectionString = getConnectionString(parentOpts);
@@ -122,7 +122,7 @@ databaseCmd
databaseCmd
.command("drop")
.description("Drop the database specified by DATABASE_URL.")
.description("Drop the database specified by POSTGRES_URL.")
.action(async () => {
const parentOpts = program.opts<{ databaseUrl?: string }>();
const connectionString = getConnectionString(parentOpts);
-1
View File
@@ -35,7 +35,6 @@ export { type priv_password_reset_token_token, type default as PrivPasswordReset
export { type song_id, type default as SongTable, type Song, type NewSong, type SongUpdate } from './public/Song';
export { type account_username_change_row_id, type default as AccountUsernameChangeTable, type AccountUsernameChange, type NewAccountUsernameChange, type AccountUsernameChangeUpdate } from './public/AccountUsernameChange';
export { type default as MigrationTable, type Migration, type NewMigration, type MigrationUpdate } from './public/Migration';
export { type account_badge_badge, type default as AccountBadgeTable, type AccountBadge, type NewAccountBadge, type AccountBadgeUpdate } from './public/AccountBadge';
export { type action_row_id, type default as ActionTable, type Action, type NewAction, type ActionUpdate } from './public/Action';
export { type priv_svc_kai_auth_token_service, type default as PrivSvcKaiAuthTokenTable, type PrivSvcKaiAuthToken, type NewPrivSvcKaiAuthToken, type PrivSvcKaiAuthTokenUpdate } from './public/PrivSvcKaiAuthToken';
export { type chart_id, type default as ChartTable, type Chart, type NewChart, type ChartUpdate } from './public/Chart';
@@ -27,9 +27,15 @@ export default interface AccountTable {
sm_twitch: ColumnType<string | null, string | null, string | null>;
bd_alpha: ColumnType<boolean, boolean | undefined, boolean>;
bd_beta: ColumnType<boolean, boolean | undefined, boolean>;
bd_dev_team: ColumnType<boolean, boolean | undefined, boolean>;
joined: ColumnType<string, string | undefined, string>;
about: ColumnType<string | null, string | null, string | null>;
about: ColumnType<string, string, string>;
status: ColumnType<string | null, string | null, string | null>;
@@ -40,6 +46,8 @@ export default interface AccountTable {
last_seen: ColumnType<string, string | undefined, string>;
auth_level: ColumnType<AuthLevel, AuthLevel | undefined, AuthLevel>;
is_supporter: ColumnType<boolean, boolean | undefined, boolean>;
}
export type Account = Selectable<AccountTable>;
@@ -36,6 +36,12 @@ export default interface PrivApiClientTable {
api_key_template: ColumnType<string | null, string | null, string | null>;
api_key_filename: ColumnType<string | null, string | null, string | null>;
webhook_uri: ColumnType<string | null, string | null, string | null>;
redirect_uri: ColumnType<string | null, string | null, string | null>;
is_builtin: ColumnType<boolean, boolean | undefined, boolean>;
}
export type PrivApiClient = Selectable<PrivApiClientTable>;
@@ -35,7 +35,6 @@ import type { default as PrivPasswordResetTokenTable } from './PrivPasswordReset
import type { default as SongTable } from './Song';
import type { default as AccountUsernameChangeTable } from './AccountUsernameChange';
import type { default as MigrationTable } from './Migration';
import type { default as AccountBadgeTable } from './AccountBadge';
import type { default as ActionTable } from './Action';
import type { default as PrivSvcKaiAuthTokenTable } from './PrivSvcKaiAuthToken';
import type { default as ChartTable } from './Chart';
@@ -139,8 +138,6 @@ export default interface PublicSchema {
_migration: MigrationTable;
account_badge: AccountBadgeTable;
action: ActionTable;
priv_svc_kai_auth_token: PrivSvcKaiAuthTokenTable;
+3
View File
@@ -13,6 +13,9 @@ const base = tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
pluginPerfectionist.configs["recommended-natural"],
{
ignores: ["**/*.oldtest.ts"],
},
{
name: "zenith/base",
files: ["**/*.{ts,tsx}"],
+1 -2
View File
@@ -1,2 +1 @@
js
!example/.env
js
File diff suppressed because one or more lines are too long
+9
View File
@@ -0,0 +1,9 @@
NODE_ENV="dev"
REDIS_URL="tachi-redis"
MONGO_URL="tachi-mongo"
PORT=8080
LOG_LEVEL="debug"
MSGPACKR_NATIVE_ACCELERATION_DISABLED=true
POSTGRES_URL="postgresql://tachi:tachi@tachi-postgres/tachi_dev"
MIGRATIONS_DIR="/tachi/db/migrations"
-1
View File
@@ -1,5 +1,4 @@
# Test stuffs
.env
.setup_env
failed-tests.txt
-6
View File
@@ -8,9 +8,3 @@ Tachi-Server is the backend for Tachi.
- Opens a public API on `/api`.
- Supports various IR frameworks under `/ir`
- Powers a ridiculously overengineered score import framework.
## Setup
Make sure you've ran the bootstrap script in `../_scripts`. You can then start the server with
`pnpm start` (in this directory) or `pnpm start-server` at the top level.
+10
View File
@@ -0,0 +1,10 @@
version: v2
inputs:
- directory: src/proto/myt
plugins:
- local: protoc-gen-es
out: src/proto/generated
opt: target=ts
- local: protoc-gen-connect-es
out: src/proto/generated
opt: target=ts
-6
View File
@@ -1,6 +0,0 @@
NODE_ENV="dev"
REDIS_URL="redis"
MONGO_URL="mongo"
PORT=8080
LOG_LEVEL="debug"
MSGPACKR_NATIVE_ACCELERATION_DISABLED=true
-98
View File
@@ -1,98 +0,0 @@
// Config file that the CI deployment uses. This is moved to $pwd/conf.json5 before CI tests are ran.
{
MONGO_DATABASE_NAME: "localdb",
CAPTCHA_SECRET_KEY: "unused",
SESSION_SECRET: "unused",
FLO_API_URL: "https://flo.example.com",
EAG_API_URL: "https://eag.example.com",
MIN_API_URL: "https://min.example.com",
MYT_API_HOST: "myt.example.com",
FLO_OAUTH2_INFO: {
CLIENT_ID: "DUMMY_CLIENT_ID",
CLIENT_SECRET: "DUMMY_CLIENT_SECRET",
REDIRECT_URI: "https://example.com",
},
EAG_OAUTH2_INFO: {
CLIENT_ID: "DUMMY_CLIENT_ID",
CLIENT_SECRET: "DUMMY_CLIENT_SECRET",
REDIRECT_URI: "https://example.com",
},
ARC_AUTH_TOKEN: "unused",
MYT_AUTH_TOKEN: "unused",
OUR_URL: "https://127.0.0.1:8080",
CLIENT_DEV_SERVER: "http://127.0.0.1:3000",
ENABLE_SERVER_HTTPS: true,
OPTIONS_ALWAYS_SUCCEEDS: true,
CDN_CONFIG: {
WEB_LOCATION: "https://127.0.0.1:8080/cdn",
SAVE_LOCATION: {
TYPE: "LOCAL_FILESYSTEM",
LOCATION: "./local-cdn",
SERVE_OWN_CDN: true,
},
},
TACHI_CONFIG: {
TYPE: "omni",
NAME: "Tachi Local Dev",
GAMES: [
"iidx",
"museca",
"sdvx",
"bms",
"chunithm",
"usc",
"wacca",
"popn",
"jubeat",
"pms",
"gitadora",
"maimai",
"maimaidx",
"itg",
"ongeki",
"ddr"
],
IMPORT_TYPES: [
"file/eamusement-iidx-csv",
"file/batch-manual",
"file/solid-state-squad",
"file/pli-iidx-csv",
"file/mypagescraper-records-csv",
"file/mypagescraper-player-csv",
"ir/direct-manual",
"ir/barbatos",
"ir/fervidex",
"ir/fervidex-static",
"ir/beatoraja",
"ir/usc",
"ir/kshook-sv6c",
"ir/lr2hook",
"api/eag-iidx",
"api/eag-sdvx",
"api/flo-iidx",
"api/flo-sdvx",
"api/min-sdvx",
"api/cg-dev-sdvx",
"api/cg-dev-popn",
"api/cg-dev-museca",
"api/cg-dev-jubeat",
"api/cg-nag-sdvx",
"api/cg-nag-popn",
"api/cg-nag-jubeat",
"api/cg-nag-museca",
"api/cg-gan-sdvx",
"api/cg-gan-popn",
"api/cg-gan-museca",
"api/cg-gan-jubeat",
"api/myt-chunithm",
"api/myt-maimaidx",
"api/myt-ongeki",
"api/myt-wacca",
],
},
SEEDS_CONFIG: {
TYPE: "LOCAL_FILES",
PATH: "../seeds/collections",
},
}
-6
View File
@@ -1,6 +0,0 @@
{
"watch": ["src"],
"ext": "ts,json",
"ignore": ["src/**/*.spec.ts"],
"exec": "ts-node ./src/main.ts"
}
+7 -8
View File
@@ -6,9 +6,7 @@
"type": "module",
"scripts": {
"dev": "bun --watch src/main.ts",
"test": "tap",
"snap": "TAP_SNAPSHOT=1 tap",
"watchtest": "tap --watch",
"test": "vitest",
"build": "tsgo -b tsconfig.build.json -v",
"typecheck": "tsgo --noEmit",
"lint": "eslint ./src",
@@ -21,8 +19,8 @@
"@types/google-protobuf": "catalog:",
"@types/pg": "catalog:",
"@types/supertest": "catalog:",
"@types/tap": "catalog:",
"@typescript/native-preview": "catalog:",
"@vitest/coverage-v8": "catalog:",
"eslint-config-tachi": "workspace:*",
"glob": "catalog:",
"google-protobuf": "catalog:",
@@ -30,9 +28,9 @@
"node-mocks-http": "catalog:",
"nodemon": "catalog:",
"supertest": "catalog:",
"tap": "workspace:*",
"ts-protoc-gen": "catalog:",
"tsconfig-paths": "catalog:"
"tsconfig-paths": "catalog:",
"vitest": "catalog:"
},
"dependencies": {
"@aws-sdk/client-s3": "catalog:",
@@ -59,6 +57,7 @@
"@types/semver": "catalog:",
"@valuabletouch/winston-seq": "catalog:",
"bcryptjs": "catalog:",
"bliss": "workspace:*",
"bms-table-loader": "catalog:",
"bullmq": "catalog:",
"bunyan": "catalog:",
@@ -77,6 +76,7 @@
"html-entities": "catalog:",
"json5": "catalog:",
"kysely": "catalog:",
"lodash": "^4.17.23",
"mkdirp": "catalog:",
"mongodb": "catalog:",
"monk": "catalog:",
@@ -86,7 +86,6 @@
"node-html-parser": "catalog:",
"nodemailer": "catalog:",
"pg": "catalog:",
"bliss": "workspace:*",
"prudence": "catalog:",
"rate-limit-redis": "catalog:",
"redis": "catalog:",
@@ -115,7 +114,7 @@
"src/*/!(test-utils)/**/*.ts"
],
"exclude": [
"src/**/*.test.ts",
"src/**/*.oldtest.ts",
"src/external/mongo/schemas.ts"
]
},
@@ -0,0 +1,67 @@
import { MakeAction } from "#lib/actions/actions.js";
import DB from "#services/pg/db.js";
import { Random20Hex } from "#utils/misc.js";
import { IsUserAdmin } from "#utils/user.js";
import { ExpectedErr } from "bliss";
export const ACTION_InstallBuiltinClient = MakeAction(
"INSTALL_BUILTIN_CLIENT",
async (
taker,
{ clientID, name, permissions, apiKeyFilename, apiKeyTemplate, redirectUri, webhookUri },
) => {
// verify taker is admin
if (!(await IsUserAdmin(taker.acct.id))) {
throw new ExpectedErr(403, "You are not authorized to perform this action.");
}
// I honestly think this is unused for builtin clients
// as they don't need to auth with anyone!
const clientSecret = `CS${Random20Hex()}`;
// create client
await DB.insertInto("priv_api_client")
.values({
client_id: clientID,
client_secret: clientSecret,
name: name,
author: taker.acct.id,
pm_customise_profile: permissions.customise_profile,
pm_customise_score: permissions.customise_score,
pm_customise_session: permissions.customise_session,
pm_delete_score: permissions.delete_score,
pm_manage_rivals: permissions.manage_rivals,
pm_manage_targets: permissions.manage_targets,
pm_submit_score: permissions.submit_score,
pm_manage_challenges: permissions.manage_challenges,
api_key_filename: apiKeyFilename,
api_key_template: apiKeyTemplate,
webhook_uri: webhookUri,
redirect_uri: redirectUri,
is_builtin: true,
})
.onConflict((oc) =>
oc.doUpdateSet({
client_secret: clientSecret,
name: name,
author: taker.acct.id,
pm_customise_profile: permissions.customise_profile,
pm_customise_score: permissions.customise_score,
pm_customise_session: permissions.customise_session,
pm_delete_score: permissions.delete_score,
pm_manage_rivals: permissions.manage_rivals,
pm_manage_targets: permissions.manage_targets,
pm_submit_score: permissions.submit_score,
pm_manage_challenges: permissions.manage_challenges,
api_key_filename: apiKeyFilename,
api_key_template: apiKeyTemplate,
webhook_uri: webhookUri,
redirect_uri: redirectUri,
is_builtin: true,
}),
)
.execute();
return {};
},
);
@@ -2,7 +2,7 @@ import type { DeepPartial } from "#utils/types";
import { log } from "#lib/log/log.js";
import { CreatePBDoc } from "#lib/score-import/framework/pb/create-pb-doc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkMockPB, mkMockScore } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { TestSnapshot } from "#test-utils/single-process-snapshot";
@@ -146,8 +146,8 @@ t.test("Arcaea Implementation", (t) => {
t.beforeEach(ResetDBState);
t.test("Should join best lamp", async (t) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestLamp",
scoreData: {
@@ -1,6 +1,6 @@
import { log } from "#lib/log/log.js";
import { CreatePBDoc } from "#lib/score-import/framework/pb/create-pb-doc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkMockPB, mkMockScore } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { TestSnapshot } from "#test-utils/single-process-snapshot";
@@ -297,10 +297,10 @@ for (const [game, playtype, impl] of [
t.test("PB Mergers", (t) => {
t.beforeEach(ResetDBState);
t.beforeEach(() => db.scores.insert(mockScore));
t.beforeEach(() => MONGODB_KILL.scores.insert(mockScore));
t.test("Should join best lamp", async (t) => {
await db.scores.insert(
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestLamp",
scoreData: {
@@ -326,7 +326,7 @@ for (const [game, playtype, impl] of [
});
t.test("Should join best BP", async (t) => {
await db.scores.insert([
await MONGODB_KILL.scores.insert([
dmf(mockScore, {
scoreID: "whateverBP",
scoreData: { optional: { bp: 100 } },
@@ -1,6 +1,6 @@
import { log } from "#lib/log/log.js";
import { CreatePBDoc } from "#lib/score-import/framework/pb/create-pb-doc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkMockPB, mkMockScore } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { CHUNITHMBBKKChart, TestingChunithmScorePB } from "#test-utils/test-data";
@@ -83,7 +83,7 @@ t.test("CHUNITHM Implementation", (t) => {
const mockPBs = async (ratings: Array<number>) => {
await Promise.all(
ratings.map((rating, idx) =>
db["personal-bests"].insert({
MONGODB_KILL["personal-bests"].insert({
...TestingChunithmScorePB,
chartID: `TEST${idx}`,
calculatedData: {
@@ -192,8 +192,8 @@ t.test("CHUNITHM Implementation", (t) => {
t.beforeEach(ResetDBState);
t.test("Should join best lamp", async (t) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestNoteLamp",
scoreData: {
@@ -203,7 +203,7 @@ t.test("CHUNITHM Implementation", (t) => {
},
}),
);
await db.scores.insert(
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestClearLamp",
scoreData: {
@@ -6,7 +6,7 @@ import type {
ScoreValidator,
} from "#game-implementations/types";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { DDRFlare } from "rg-stats";
import {
type ChartDocument,
@@ -157,7 +157,7 @@ const DDR_CALCULATE_FLARE_SKILL: GPTNewProfileCalcs<"ddr:DP" | "ddr:SP"> = async
playtype,
userID,
) => {
const sc: Array<PBScoreDocumentWithSong> = await db["personal-bests"].aggregate([
const sc: Array<PBScoreDocumentWithSong> = await MONGODB_KILL["personal-bests"].aggregate([
{
$match: {
userID,
@@ -1,6 +1,6 @@
import { log } from "#lib/log/log.js";
import { CreatePBDoc } from "#lib/score-import/framework/pb/create-pb-doc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkMockPB, mkMockScore } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { TestingGitadoraChart } from "#test-utils/test-data";
@@ -170,8 +170,8 @@ for (const [playtype, impl] of [
t.beforeEach(ResetDBState);
t.test("Should join best lamp", async (t) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestLamp",
scoreData: {
@@ -1,6 +1,6 @@
import { log } from "#lib/log/log.js";
import { CreatePBDoc } from "#lib/score-import/framework/pb/create-pb-doc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkFakePBIIDXSP, mkFakeScoreIIDXSP } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { TestSnapshot } from "#test-utils/single-process-snapshot";
@@ -399,7 +399,7 @@ t.test("IIDX Implementation", (t) => {
for (const gptStr of ["iidx:SP", "iidx:DP"] as const) {
t.test("Should join best lamp", async (t) => {
await db.scores.insert(
await MONGODB_KILL.scores.insert(
mkFakeScoreIIDXSP({
scoreID: "bestLamp",
scoreData: {
@@ -428,7 +428,7 @@ t.test("IIDX Implementation", (t) => {
});
t.test("Should join best BP", async (t) => {
await db.scores.insert([
await MONGODB_KILL.scores.insert([
mkFakeScoreIIDXSP({
scoreID: "whateverBP",
scoreData: { optional: { bp: 100 } },
@@ -2,7 +2,7 @@ import type { ProvidedMetrics, ScoreData } from "tachi-common";
import { log } from "#lib/log/log.js";
import { CreatePBDoc } from "#lib/score-import/framework/pb/create-pb-doc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkMockPB, mkMockScore } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { TestSnapshot } from "#test-utils/single-process-snapshot";
@@ -204,8 +204,8 @@ t.test("Jubeat Implementation", (t) => {
t.beforeEach(ResetDBState);
t.test("Should join best lamp", async (t) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestLamp",
scoreData: {
@@ -234,8 +234,8 @@ t.test("Jubeat Implementation", (t) => {
});
t.test("Should join best score", async (t) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestScore",
scoreData: {
@@ -3,7 +3,7 @@ import type { GPTServerImplementation } from "#game-implementations/types";
import { CreatePBMergeFor } from "#game-implementations/utils/pb-merge";
import { ProfileSumBestN } from "#game-implementations/utils/profile-calc";
import { SessionAvgBest10For } from "#game-implementations/utils/session-calc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { IsNullish } from "#utils/misc";
import { p } from "prudence";
import { Jubility } from "rg-stats";
@@ -27,7 +27,7 @@ async function GetBestJubilityOnSongs(
playtype: Playtype,
limit: integer,
): Promise<Array<PBScoreDocument>> {
const r: Array<{ doc: PBScoreDocument }> = await db["personal-bests"].aggregate([
const r: Array<{ doc: PBScoreDocument }> = await MONGODB_KILL["personal-bests"].aggregate([
{
$match: {
game,
@@ -107,14 +107,14 @@ async function GetBestJubilityOnSongs(
const CURRENT_JUBEAT_HOT_VERSION: Versions["jubeat:Single"] = "ave";
export async function GetPBsForJubility(userID: integer) {
const hotSongs = await db.songs.jubeat.find(
const hotSongs = await MONGODB_KILL.songs.jubeat.find(
{ "data.displayVersion": CURRENT_JUBEAT_HOT_VERSION },
{ projection: { id: 1 } },
);
const hotSongIDs = hotSongs.map((e) => e.id);
const coldSongs = await db.songs.jubeat.find(
const coldSongs = await MONGODB_KILL.songs.jubeat.find(
{ "data.displayVersion": { $ne: CURRENT_JUBEAT_HOT_VERSION } },
{ projection: { id: 1 } },
);
@@ -2,7 +2,7 @@ import type { integer, ProvidedMetrics, ScoreData } from "tachi-common";
import { log } from "#lib/log/log.js";
import { CreatePBDoc } from "#lib/score-import/framework/pb/create-pb-doc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkMockPB, mkMockScore } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { TestSnapshot } from "#test-utils/single-process-snapshot";
@@ -167,8 +167,8 @@ t.test("maimai Implementation", (t) => {
t.beforeEach(ResetDBState);
t.test("Should join best lamp", async (t) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestLamp",
scoreData: {
@@ -2,7 +2,7 @@ import type { ProvidedMetrics, ScoreData } from "tachi-common";
import { log } from "#lib/log/log.js";
import { CreatePBDoc } from "#lib/score-import/framework/pb/create-pb-doc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkMockPB, mkMockScore } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { TestingMaimaiDXChart } from "#test-utils/test-data";
@@ -134,8 +134,8 @@ t.test("Maimai DX Implementation", (t) => {
t.beforeEach(ResetDBState);
t.test("Should join best lamp", async (t) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestLamp",
scoreData: {
@@ -1,6 +1,6 @@
import { log } from "#lib/log/log.js";
import { CreatePBDoc } from "#lib/score-import/framework/pb/create-pb-doc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkMockPB, mkMockScore } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { TestingMusecaChart } from "#test-utils/test-data";
@@ -100,8 +100,8 @@ t.test("MUSECA Implementation", (t) => {
t.beforeEach(ResetDBState);
t.test("Should join best lamp", async (t) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestLamp",
scoreData: {
@@ -2,7 +2,7 @@
import { log } from "#lib/log/log.js";
import { CreatePBDoc, UpdateChartRanking } from "#lib/score-import/framework/pb/create-pb-doc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkMockPB, mkMockScore } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { TestingOngekiChart, TestingOngekiScorePB } from "#test-utils/test-data";
@@ -136,7 +136,7 @@ t.test("ONGEKI Implementation", (t: any) => {
? { rating: 0, scoreRating: rating, starRating: 0 }
: { rating: 0, scoreRating: 0, starRating: rating };
return db["personal-bests"].insert({
return MONGODB_KILL["personal-bests"].insert({
...TestingOngekiScorePB,
chartID: `TEST${field}${idx}`,
calculatedData: {
@@ -282,8 +282,8 @@ t.test("ONGEKI Implementation", (t: any) => {
t.beforeEach(ResetDBState);
t.test("Should join best lamp", async (t: any) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestLamp",
scoreData: {
@@ -314,8 +314,8 @@ t.test("ONGEKI Implementation", (t: any) => {
});
t.test("Should join platinum score", async (t: any) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestPlatinum",
scoreData: {
@@ -348,7 +348,7 @@ t.test("ONGEKI Implementation", (t: any) => {
t.beforeEach(ResetDBState);
t.test("Should tiebreak according to platinum score", async (t: any) => {
await db["personal-bests"].insert({
await MONGODB_KILL["personal-bests"].insert({
...TestingOngekiScorePB,
scoreData: {
...TestingOngekiScorePB.scoreData,
@@ -358,7 +358,7 @@ t.test("ONGEKI Implementation", (t: any) => {
userID: 1,
});
await db["personal-bests"].insert({
await MONGODB_KILL["personal-bests"].insert({
...TestingOngekiScorePB,
scoreData: {
...TestingOngekiScorePB.scoreData,
@@ -368,7 +368,7 @@ t.test("ONGEKI Implementation", (t: any) => {
userID: 2,
});
await db["personal-bests"].insert({
await MONGODB_KILL["personal-bests"].insert({
...TestingOngekiScorePB,
scoreData: {
...TestingOngekiScorePB.scoreData,
@@ -380,7 +380,7 @@ t.test("ONGEKI Implementation", (t: any) => {
await UpdateChartRanking("ongeki", "Single", TestingOngekiChart.chartID);
const pbs = (await db["personal-bests"].find({
const pbs = (await MONGODB_KILL["personal-bests"].find({
chartID: TestingOngekiChart.chartID,
})) as Array<PBScoreDocument<"ongeki:Single">>;
@@ -401,7 +401,7 @@ t.test("ONGEKI Implementation", (t: any) => {
});
t.test("Should not tiebreak if tech scores differ", async (t: any) => {
await db["personal-bests"].insert({
await MONGODB_KILL["personal-bests"].insert({
...TestingOngekiScorePB,
scoreData: {
...TestingOngekiScorePB.scoreData,
@@ -411,7 +411,7 @@ t.test("ONGEKI Implementation", (t: any) => {
userID: 1,
});
await db["personal-bests"].insert({
await MONGODB_KILL["personal-bests"].insert({
...TestingOngekiScorePB,
scoreData: {
...TestingOngekiScorePB.scoreData,
@@ -423,7 +423,7 @@ t.test("ONGEKI Implementation", (t: any) => {
await UpdateChartRanking("ongeki", "Single", TestingOngekiChart.chartID);
const [pb1, pb2] = (await db["personal-bests"].find(
const [pb1, pb2] = (await MONGODB_KILL["personal-bests"].find(
{
chartID: TestingOngekiChart.chartID,
},
@@ -2,7 +2,7 @@ import type { ProvidedMetrics, ScoreData } from "tachi-common";
import { log } from "#lib/log/log.js";
import { CreatePBDoc } from "#lib/score-import/framework/pb/create-pb-doc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkMockPB, mkMockScore } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { TestingPopnChart } from "#test-utils/test-data";
@@ -159,8 +159,8 @@ t.test("Pop'n Implementation", (t) => {
t.beforeEach(ResetDBState);
t.test("Should join best clear medal", async (t) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestClearMedal",
scoreData: {
@@ -1,6 +1,6 @@
import { log } from "#lib/log/log.js";
import { CreatePBDoc } from "#lib/score-import/framework/pb/create-pb-doc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkMockPB, mkMockScore } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { TestSnapshot } from "#test-utils/single-process-snapshot";
@@ -172,8 +172,8 @@ t.test("SDVX Implementation", (t) => {
t.beforeEach(ResetDBState);
t.test("Should join best lamp", async (t) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestLamp",
scoreData: {
@@ -197,8 +197,8 @@ t.test("SDVX Implementation", (t) => {
});
t.test("Should join best EX SCore", async (t) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestLamp",
scoreData: {
@@ -1,6 +1,6 @@
import { log } from "#lib/log/log.js";
import { CreatePBDoc } from "#lib/score-import/framework/pb/create-pb-doc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkMockPB, mkMockScore } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { TestingUSCChart } from "#test-utils/test-data";
@@ -166,8 +166,8 @@ for (const [playtype, impl] of [
t.beforeEach(ResetDBState);
t.test("Should join best lamp", async (t) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestLamp",
scoreData: {
@@ -3,7 +3,7 @@ import type { ProvidedMetrics, ScoreData, ScoreDocument } from "tachi-common";
import { log } from "#lib/log/log.js";
import { CreatePBDoc } from "#lib/score-import/framework/pb/create-pb-doc";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkMockPB, mkMockScore } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { TestSnapshot } from "#test-utils/single-process-snapshot";
@@ -144,8 +144,8 @@ t.test("WACCA Implementation", (t) => {
t.beforeEach(ResetDBState);
t.test("Should join best lamp", async (t) => {
await db.scores.insert(mockScore);
await db.scores.insert(
await MONGODB_KILL.scores.insert(mockScore);
await MONGODB_KILL.scores.insert(
dmf(mockScore, {
scoreID: "bestLamp",
scoreData: {
@@ -10,7 +10,7 @@ import type {
} from "tachi-common";
import type { ExtractEnumMetricNames } from "tachi-common/types/metrics";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
// insane typemagic to get mongodb-safe names for this GPT's metrics.
type MetricKeys<GPT extends GPTString> = Exclude<
@@ -55,7 +55,7 @@ export function CreatePBMergeFor<GPT extends GPTString>(
applicator: (base: PBScoreDocumentNoRank<GPT>, score: ScoreDocument<GPT>) => void,
): PBMergeFunction<GPT> {
return async (userID, chartID, asOfTimestamp, base) => {
const bestScoreFor = (await db.scores.findOne(
const bestScoreFor = (await MONGODB_KILL.scores.findOne(
HandleAsOf(
{
userID,
@@ -7,7 +7,7 @@ import type {
ScoreRatingAlgorithms,
} from "tachi-common";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
/**
* Curries a function that returns the sum of N best ratings on `key`.
@@ -29,7 +29,7 @@ function CalcN<GPT extends GPTString>(
multiplier = 1,
) {
return async (game: GameGroup, playtype: Playtype, userID: integer) => {
const sc = await db["personal-bests"].find(
const sc = await MONGODB_KILL["personal-bests"].find(
{
game,
playtype,
@@ -100,7 +100,7 @@ export async function GetBestRatingOnSongs(
ratingProp: "skill",
limit: integer,
): Promise<Array<PBScoreDocument>> {
const r: Array<{ doc: PBScoreDocument }> = await db["personal-bests"].aggregate([
const r: Array<{ doc: PBScoreDocument }> = await MONGODB_KILL["personal-bests"].aggregate([
{
$match: {
game,
+27 -3
View File
@@ -1,6 +1,7 @@
import db from "#services/pg/db";
import DB from "#services/pg/db";
import { type ActionTaker, type AnonActionTaker, MakeActionGuts } from "bliss";
import { type ActionSignature } from "bliss/actions";
import { zodPermission } from "tachi-common";
import { z } from "zod";
const APP_NAME = "TACHI_SERVER";
@@ -13,6 +14,29 @@ export const ActionSignatures = {
input: z.object({}),
output: z.object({}),
},
INSTALL_BUILTIN_CLIENT: {
input: z.object({
clientID: z.string(),
name: z.string(),
webhookUri: z.url().nullable(),
redirectUri: z.url().nullable(),
permissions: z
.object({
customise_profile: z.boolean(),
customise_score: z.boolean(),
customise_session: z.boolean(),
delete_score: z.boolean(),
manage_rivals: z.boolean(),
manage_targets: z.boolean(),
submit_score: z.boolean(),
manage_challenges: z.boolean(),
})
.partial(),
apiKeyTemplate: z.string().nullable(),
apiKeyFilename: z.string().nullable(),
}),
output: z.object({}),
},
} satisfies Record<string, ActionSignature>;
export const AnonActionSignatures = {
@@ -30,7 +54,7 @@ export function MakeAnonAction<A extends AnonActionName>(
fn: AnonActionFn<A>,
): AnonActionFn<A> {
return MakeActionGuts({
db,
db: DB,
appName: APP_NAME,
kind,
// @ts-expect-error we're being creative with the types here
@@ -44,7 +68,7 @@ export function MakeAnonAction<A extends AnonActionName>(
*/
export function MakeAction<A extends ActionName>(kind: A, fn: ActionFn<A>): ActionFn<A> {
return MakeActionGuts({
db,
db: DB,
appName: APP_NAME,
kind,
// @ts-expect-error we're being creative with the types here
@@ -16,7 +16,7 @@ import type {
UserDocument,
} from "tachi-common";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import {
GetRecentlyAchievedGoals,
GetRecentlyAchievedQuests,
@@ -79,7 +79,7 @@ export async function GetRecentActivity(
timeStarted: { $lt: startFrom === null ? Infinity : startFrom },
};
const recentSessions = await db.sessions.find(initialSessionQuery, {
const recentSessions = await MONGODB_KILL.sessions.find(initialSessionQuery, {
sort: {
timeStarted: -1,
},
@@ -108,7 +108,7 @@ export async function GetRecentActivity(
{ goals, goalSubs },
{ quests, questSubs },
] = await Promise.all([
db["class-achievements"].find(
MONGODB_KILL["class-achievements"].find(
{
...baseQuery,
timeAchieved: timeConstraint,
@@ -119,7 +119,7 @@ export async function GetRecentActivity(
},
},
),
db.scores.find(
MONGODB_KILL.scores.find(
{
...baseQuery,
highlight: true,
@@ -1,13 +1,11 @@
import type { PrudenceError } from "prudence";
import { ACTION_InstallBuiltinClient } from "#actions/install-builtin-client.js";
import { log } from "#lib/log/log.js";
import { ServerConfig, TachiConfig } from "#lib/setup/config";
import { type TachiAPIClientDocument, UserAuthLevels } from "tachi-common";
import _ from "lodash";
import { type TachiAPIClientDocument } from "tachi-common";
/* eslint-disable no-await-in-loop */
import db from "#services/mongo/db";
import { DatabaseSchemas } from "#services/mongo/schemas";
import { Random20Hex } from "#utils/misc";
import { FormatPrError } from "#utils/prudence";
import { GetClientByID } from "#utils/queries/api-clients.js";
import { GetFirstAdmin } from "#utils/user.js";
import fjsh from "fast-json-stable-hash";
type DefaultClients = Array<Omit<TachiAPIClientDocument, "author" | "clientSecret">>;
@@ -307,56 +305,48 @@ export async function LoadDefaultClients() {
}
async function LoadClients(clients: DefaultClients) {
const firstAdmin = await db.users.findOne({
authLevel: UserAuthLevels.ADMIN,
});
if (!firstAdmin) {
log.warn(`There are no admins on this instance of tachi-server. We cannot create default API Clients!
Chances are, you're seeing this message because you just bootstrapped Tachi.
You'll need to set a user's authLevel to ${UserAuthLevels.ADMIN}.
If you have no users, go create an account using the frontend, then run pnpm make-user-admin 1.`);
return;
}
const firstAdmin = await GetFirstAdmin();
for (const client of clients) {
const exists = await db["api-clients"].findOne(
const exists = await GetClientByID(client.clientID);
if (exists) {
const existsOmitted = _.omit(exists, "author", "clientSecret");
// Skip if nothing has changed.
if (fjsh.hash(existsOmitted, "sha256") === fjsh.hash(client, "sha256")) {
continue;
}
}
await ACTION_InstallBuiltinClient(
{
clientID: client.clientID,
},
{
projection: {
clientSecret: 0,
author: 0,
ip: null,
acct: {
id: firstAdmin.id,
username: firstAdmin.username,
},
},
{
clientID: client.clientID,
name: client.name,
permissions: {
customise_profile: false,
customise_score: false,
customise_session: false,
delete_score: false,
manage_rivals: false,
manage_targets: false,
submit_score: false,
manage_challenges: false,
},
apiKeyFilename: client.apiKeyFilename,
apiKeyTemplate: client.apiKeyTemplate,
redirectUri: client.redirectUri,
webhookUri: client.webhookUri,
},
);
// Skip if nothing has changed.
if (fjsh.hash(exists, "sha256") === fjsh.hash(client, "sha256")) {
continue;
}
const realClient: TachiAPIClientDocument = {
...client,
clientSecret: `CS${Random20Hex()}`,
author: 1,
};
try {
DatabaseSchemas["api-clients"](realClient);
} catch (err) {
log.error(`Invalid API Client ${client.name}: ${FormatPrError(err as PrudenceError)}.`);
continue;
}
// No replaceOne support in monk -- have to do this.
await db["api-clients"].remove({
clientID: client.clientID,
});
await db["api-clients"].insert(realClient);
log.info(`Loaded/Modified new built-in client ${client.name}.`);
}
}
@@ -0,0 +1,64 @@
import { type Selection } from "kysely";
import { type APIPermissions, type TachiAPIClientDocument } from "tachi-common";
import { type Database } from "tachi-db";
export const SELECT_API_CLIENT = [
"priv_api_client.client_id",
"priv_api_client.client_secret",
"priv_api_client.name",
"priv_api_client.author",
"priv_api_client.pm_customise_profile",
"priv_api_client.pm_customise_score",
"priv_api_client.pm_customise_session",
"priv_api_client.pm_delete_score",
"priv_api_client.pm_manage_rivals",
"priv_api_client.pm_manage_targets",
"priv_api_client.pm_submit_score",
"priv_api_client.pm_manage_challenges",
"priv_api_client.api_key_filename",
"priv_api_client.api_key_template",
"priv_api_client.webhook_uri",
"priv_api_client.redirect_uri",
] as const;
export function ToAPIClientDocument(
row: Selection<Database, "priv_api_client", (typeof SELECT_API_CLIENT)[number]>,
): TachiAPIClientDocument {
const requestedPermissions: Array<APIPermissions> = [];
if (row.pm_customise_profile) {
requestedPermissions.push("customise_profile");
}
if (row.pm_customise_score) {
requestedPermissions.push("customise_score");
}
if (row.pm_customise_session) {
requestedPermissions.push("customise_session");
}
if (row.pm_delete_score) {
requestedPermissions.push("delete_score");
}
if (row.pm_manage_rivals) {
requestedPermissions.push("manage_rivals");
}
if (row.pm_manage_targets) {
requestedPermissions.push("manage_targets");
}
if (row.pm_submit_score) {
requestedPermissions.push("submit_score");
}
if (row.pm_manage_challenges) {
requestedPermissions.push("manage_challenges");
}
return {
clientID: row.client_id,
clientSecret: row.client_secret,
name: row.name,
author: row.author,
apiKeyFilename: row.api_key_filename,
apiKeyTemplate: row.api_key_template,
redirectUri: row.redirect_uri,
requestedPermissions,
webhookUri: row.webhook_uri,
};
}
@@ -0,0 +1,29 @@
import { type Selection } from "kysely";
import { type integer, type UserSettingsDocument } from "tachi-common";
import { type Database } from "tachi-db";
export const SELECT_USER_SETTINGS = [
"account_settings.user_id",
"account_settings.pf_invisible",
"account_settings.pf_developer_mode",
"account_settings.pf_advanced_mode",
"account_settings.pf_contentious_content",
"account_settings.pf_deletable_scores",
] as const;
export function ToUserSettingsDocument(
following: Array<integer>,
row: Selection<Database, "account_settings", (typeof SELECT_USER_SETTINGS)[number]>,
): UserSettingsDocument {
return {
userID: row.user_id,
following,
preferences: {
invisible: row.pf_invisible,
developerMode: row.pf_developer_mode,
advancedMode: row.pf_advanced_mode,
contentiousContent: row.pf_contentious_content,
deletableScores: row.pf_deletable_scores,
},
};
}
@@ -0,0 +1,67 @@
import { AuthLevelToInt } from "#utils/conversion.js";
import { ISO8601ToUnixMilliseconds } from "#utils/time.js";
import { type Selection } from "kysely";
import { type UserBadges, type UserDocument } from "tachi-common";
import { type Database } from "tachi-db";
export const SELECT_USER = [
"account.id",
"account.bd_alpha",
"account.bd_beta",
"account.bd_dev_team",
"account.about",
"account.auth_level",
"account.custom_banner_location",
"account.custom_pfp_location",
"account.joined",
"account.last_seen",
"account.sm_discord",
"account.sm_twitter",
"account.sm_github",
"account.sm_steam",
"account.sm_youtube",
"account.sm_twitch",
"account.status",
"account.username",
"account.normalized_username",
"account.is_supporter",
] as const;
export function ToUserDocument(
row: Selection<Database, "account", (typeof SELECT_USER)[number]>,
): UserDocument {
const badges: Array<UserBadges> = [];
if (row.bd_alpha) {
badges.push("alpha");
}
if (row.bd_beta) {
badges.push("beta");
}
if (row.bd_dev_team) {
badges.push("dev-team");
}
return {
about: row.about,
authLevel: AuthLevelToInt(row.auth_level),
badges,
customBannerLocation: row.custom_banner_location,
customPfpLocation: row.custom_pfp_location,
id: row.id,
joinDate: ISO8601ToUnixMilliseconds(row.joined),
lastSeen: ISO8601ToUnixMilliseconds(row.last_seen),
socialMedia: {
discord: row.sm_discord,
twitter: row.sm_twitter,
github: row.sm_github,
steam: row.sm_steam,
youtube: row.sm_youtube,
twitch: row.sm_twitch,
},
status: row.status,
username: row.username,
usernameLowercase: row.normalized_username,
isSupporter: row.is_supporter,
};
}
@@ -1,4 +1,4 @@
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import ResetDBState from "#test-utils/resets";
import t from "tap";
@@ -10,7 +10,7 @@ t.test("#CreateGameSettings", (t) => {
t.test("Should create a new user's settings.", async (t) => {
await CreateGameSettings(1, "bms", "7K");
const data = await db["game-settings"].findOne({
const data = await MONGODB_KILL["game-settings"].findOne({
userID: 1,
game: "bms",
playtype: "7K",
@@ -1,13 +1,13 @@
import type { GameGroup, integer, Playtype } from "tachi-common";
import { log } from "#lib/log/log.js";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
/**
* Create GameSettings for a UGPT (which contains their preferences).
*/
export async function CreateGameSettings(userID: integer, game: GameGroup, playtype: Playtype) {
const exists = await db["game-settings"].findOne({
const exists = await MONGODB_KILL["game-settings"].findOne({
userID,
game,
playtype,
@@ -32,7 +32,7 @@ export async function CreateGameSettings(userID: integer, game: GameGroup, playt
};
}
await db["game-settings"].insert({
await MONGODB_KILL["game-settings"].insert({
userID,
game,
playtype,
@@ -1,7 +1,7 @@
import type { ChartDocument, integer, Playtypes } from "tachi-common";
import { log } from "#lib/log/log.js";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { PoyashiBPI } from "rg-stats";
interface PlaylistEntry {
@@ -110,7 +110,7 @@ export const CUSTOM_TACHI_IIDX_PLAYLISTS: Array<TachiIIDXPlaylist> = [
bounds.push([cutoffs[i], cutoffs[i + 1]] as [number, number]);
}
const charts = (await db.charts.iidx.find({
const charts = (await MONGODB_KILL.charts.iidx.find({
"data.kaidenAverage": { $ne: null },
playtype,
})) as Array<ChartDocument<"iidx:DP" | "iidx:SP">>;
@@ -1,4 +1,4 @@
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf, mkFakeImport } from "#test-utils/misc";
import ResetDBState from "#test-utils/resets";
import { TestingIIDXSPScore } from "#test-utils/test-data";
@@ -9,7 +9,7 @@ import { RevertImport } from "./imports";
t.test("#RevertImport", (t) => {
t.beforeEach(ResetDBState);
t.beforeEach(() =>
db.scores.insert([
MONGODB_KILL.scores.insert([
dmf(TestingIIDXSPScore, { scoreID: "score_1" }),
dmf(TestingIIDXSPScore, { scoreID: "score_2" }),
dmf(TestingIIDXSPScore, { scoreID: "score_3" }),
@@ -21,16 +21,16 @@ t.test("#RevertImport", (t) => {
scoreIDs: ["score_1", "score_2"],
});
await db.imports.insert(importDoc);
await MONGODB_KILL.imports.insert(importDoc);
await RevertImport(importDoc);
const dbRes = await db.imports.findOne({ importID: "fake_import" });
const dbRes = await MONGODB_KILL.imports.findOne({ importID: "fake_import" });
t.equal(dbRes, null, "Should have removed the import from the DB.");
t.resolveMatch(
db.scores.findOne({ userID: 1, scoreID: "score_1" }),
MONGODB_KILL.scores.findOne({ userID: 1, scoreID: "score_1" }),
// @ts-expect-error https://github.com/DefinitelyTyped/DefinitelyTyped/pull/60020
null,
@@ -38,7 +38,7 @@ t.test("#RevertImport", (t) => {
);
t.resolveMatch(
db.scores.findOne({ userID: 1, scoreID: "score_2" }),
MONGODB_KILL.scores.findOne({ userID: 1, scoreID: "score_2" }),
// @ts-expect-error see above
null,
@@ -46,7 +46,7 @@ t.test("#RevertImport", (t) => {
);
t.resolveMatch(
db.scores.findOne({ userID: 1, scoreID: "score_3" }),
MONGODB_KILL.scores.findOne({ userID: 1, scoreID: "score_3" }),
// @ts-expect-error see above
{ scoreID: "score_3" },
+3 -3
View File
@@ -6,7 +6,7 @@ import {
UnsetOngoingImportLock,
} from "#lib/score-import/framework/import-locks/lock";
import { DeleteMultipleScores } from "#lib/score-mutation/delete-scores";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
interface OngoingImportError {
tag: "ONGOING_IMPORT";
@@ -43,7 +43,7 @@ export async function RevertImport(importDoc: ImportDocument): Promise<OngoingIm
);
try {
await db.imports.remove({ importID: importDoc.importID });
await MONGODB_KILL.imports.remove({ importID: importDoc.importID });
log.info(`Reverted and deleted import '${importDoc.importID}'.`);
} catch (err) {
@@ -63,5 +63,5 @@ export async function RevertImport(importDoc: ImportDocument): Promise<OngoingIm
* Retrieve the scores inside this import.
*/
export function GetImportScores(importDoc: ImportDocument) {
return db.scores.find({ scoreID: { $in: importDoc.scoreIDs } });
return MONGODB_KILL.scores.find({ scoreID: { $in: importDoc.scoreIDs } });
}
@@ -1,7 +1,7 @@
import { log } from "#lib/log/log.js";
import { PullDatabaseSeeds } from "#lib/seeds/repo";
/* eslint-disable no-await-in-loop */
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { WrapScriptPromise } from "#utils/misc";
/**
@@ -15,7 +15,7 @@ export async function BacksyncBMSPMSSongsAndCharts() {
log.info(`Fetching ${game} songs from DB.`);
// did you know, this code is liable to blow up in my face and OOM one day?
let songs = await db.anySongs[game].find({});
let songs = await MONGODB_KILL.anySongs[game].find({});
log.info(`Found ${songs.length} ${game} songs.`);
@@ -27,7 +27,7 @@ export async function BacksyncBMSPMSSongsAndCharts() {
songs = null;
log.info(`Fetching ${game} charts from DB.`);
let charts = await db.anyCharts[game].find({});
let charts = await MONGODB_KILL.anyCharts[game].find({});
log.info(`Found ${charts.length} ${game} charts.`);
@@ -3,7 +3,7 @@ import type { FilterQuery } from "mongodb";
import { log } from "#lib/log/log.js";
import { DeorphanIfInQueue } from "#lib/orphan-queue/orphan-queue";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { InitaliseFolderChartLookup } from "#utils/folder";
import { FormatBMSTables, WrapScriptPromise } from "#utils/misc";
import { type BMSTableEntry, LoadBMSTable } from "bms-table-loader";
@@ -34,7 +34,7 @@ async function HandleTableRemovals(
// *unless this script crashes, in which case it's
// no longer temporary
const existingCharts = (await db.charts.bms.find({
const existingCharts = (await MONGODB_KILL.charts.bms.find({
playtype,
"data.tableFolders.table": prefix,
})) as unknown as Array<ChartDocument<"bms:7K" | "bms:14K">>;
@@ -85,7 +85,7 @@ async function HandleTableRemovals(
// remove this table info from all of the charts that no longer
// exist in the table.
await db.charts.bms.update(
await MONGODB_KILL.charts.bms.update(
{
chartID: { $in: toRemove },
},
@@ -115,7 +115,7 @@ async function ImportTableLevels(
.filter((e) => e.checksum.type === "md5")
.map((e) => e.checksum.value);
await db.charts.bms.update(
await MONGODB_KILL.charts.bms.update(
{
"data.hashMD5": { $in: md5s },
},
@@ -127,7 +127,7 @@ async function ImportTableLevels(
{ multi: true },
);
await db.charts.bms.update(
await MONGODB_KILL.charts.bms.update(
{
"data.hashSHA256": { $in: sha256s },
},
@@ -154,7 +154,8 @@ async function ImportTableLevels(
}
}
let chart: ChartDocument<"bms:7K" | "bms:14K"> | null = await db.charts.bms.findOne(query);
let chart: ChartDocument<"bms:7K" | "bms:14K"> | null =
await MONGODB_KILL.charts.bms.findOne(query);
if (!chart) {
// didn't find it in the DB?
@@ -195,7 +196,7 @@ async function ImportTableLevels(
return a.level.localeCompare(b.level);
});
await db.charts.bms.update(
await MONGODB_KILL.charts.bms.update(
{
chartID: chart.chartID,
},
@@ -206,7 +207,7 @@ async function ImportTableLevels(
},
);
await db.songs.bms.update(
await MONGODB_KILL.songs.bms.update(
{
id: chart.songID,
},
+13 -7
View File
@@ -1,7 +1,7 @@
import type { UserGameStats, UserGameStatsSnapshotDocument } from "tachi-common";
import { log } from "#lib/log/log.js";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { GetMillisecondsSince } from "#utils/misc";
import { GetAllRankings } from "#utils/user";
@@ -16,7 +16,9 @@ let batchWrite: Array<UserGameStatsSnapshotDocument> = [];
export async function UGSSnapshot() {
const timeStart = process.hrtime.bigint();
const alreadyExists = await db["game-stats-snapshots"].findOne({ timestamp: currentTime });
const alreadyExists = await MONGODB_KILL["game-stats-snapshots"].findOne({
timestamp: currentTime,
});
if (alreadyExists) {
log.warn(
@@ -31,7 +33,7 @@ export async function UGSSnapshot() {
log.info(`Snapshotting UserGameStats.`);
try {
await db["game-stats"]
await MONGODB_KILL["game-stats"]
.find({})
// @ts-expect-error faulty TS types
@@ -41,7 +43,11 @@ export async function UGSSnapshot() {
log.debug(`Snapshotting ${ugs.userID} ${ugs.playtype} ${ugs.game}.`);
const [playcount, rankings] = await Promise.all([
db.scores.count({ userID: ugs.userID, playtype: ugs.playtype, game: ugs.game }),
MONGODB_KILL.scores.count({
userID: ugs.userID,
playtype: ugs.playtype,
game: ugs.game,
}),
GetAllRankings(ugs),
]);
@@ -56,7 +62,7 @@ export async function UGSSnapshot() {
if (batchWrite.length >= 500) {
log.debug(`Flushed batch.`);
await db["game-stats-snapshots"].insert(batchWrite);
await MONGODB_KILL["game-stats-snapshots"].insert(batchWrite);
batchWrite = [];
}
@@ -65,7 +71,7 @@ export async function UGSSnapshot() {
});
if (batchWrite.length) {
await db["game-stats-snapshots"].insert(batchWrite);
await MONGODB_KILL["game-stats-snapshots"].insert(batchWrite);
}
log.info(
@@ -84,7 +90,7 @@ export async function UGSSnapshot() {
log.info(`Removing all snapshots at this timestamp (${currentTime}).`);
await db["game-stats-snapshots"].remove({ timestamp: currentTime });
await MONGODB_KILL["game-stats-snapshots"].remove({ timestamp: currentTime });
log.info(`Removed.`);
@@ -9,7 +9,7 @@ import type {
import { log } from "#lib/log/log.js";
import { BacksyncCollection, PullDatabaseSeeds } from "#lib/seeds/repo";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { RecalcAllScores } from "#utils/calculations/recalc-scores";
import fetch from "#utils/fetch";
import { WrapScriptPromise } from "#utils/misc";
@@ -173,7 +173,7 @@ export async function UpdatePoyashiData() {
await repo.Destroy();
await BacksyncCollection("charts-iidx", db.charts.iidx, "Update BPI Data");
await BacksyncCollection("charts-iidx", MONGODB_KILL.charts.iidx, "Update BPI Data");
}
if (require.main === module) {
@@ -1,7 +1,7 @@
import { log } from "#lib/log/log.js";
import { BacksyncCollection } from "#lib/seeds/repo";
/* eslint-disable no-await-in-loop */
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { RecalcAllScores } from "#utils/calculations/recalc-scores";
import fetch from "#utils/fetch";
import { WrapScriptPromise } from "#utils/misc";
@@ -56,7 +56,7 @@ export async function UpdateDPTiers() {
continue;
}
const result = await db.charts.iidx.update(
const result = await MONGODB_KILL.charts.iidx.update(
{
versions: "29",
playtype: "DP",
@@ -92,7 +92,7 @@ export async function UpdateDPTiers() {
log.info(`Recalced those scores.`);
await BacksyncCollection("charts-iidx", db.charts.iidx, "Update DP Tierlist");
await BacksyncCollection("charts-iidx", MONGODB_KILL.charts.iidx, "Update DP Tierlist");
}
log.info("Done.");
@@ -4,7 +4,7 @@ import type { ChartDocument, Difficulties, integer } from "tachi-common";
import { log } from "#lib/log/log.js";
import { BacksyncCollection } from "#lib/seeds/repo";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { RecalcAllScores } from "#utils/calculations/recalc-scores";
import { WrapScriptPromise } from "#utils/misc";
import { FindChartWithPTDF } from "#utils/queries/charts";
@@ -172,7 +172,7 @@ async function FetchSP12Data() {
updatedChartIDs.push(chart.chartID);
await db.charts.iidx.update(
await MONGODB_KILL.charts.iidx.update(
{
chartID: chart.chartID,
},
@@ -203,7 +203,7 @@ async function FetchSP12Data() {
log.info(`Finished recalcing scores.`);
await BacksyncCollection("charts-iidx", db.charts.iidx, "Update SP12 Tierlist");
await BacksyncCollection("charts-iidx", MONGODB_KILL.charts.iidx, "Update SP12 Tierlist");
}
process.exit(0);
@@ -1,4 +1,4 @@
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import {
FormatGameGroup,
type GameGroup,
@@ -23,7 +23,7 @@ export async function SendSetRivalNotification(
game: GameGroup,
playtype: Playtype,
) {
const alreadyBeenPinged = await db.notifications.findOne({
const alreadyBeenPinged = await MONGODB_KILL.notifications.findOne({
sentTo: toUserID,
"body.type": "RIVALED_BY",
"body.content": {
@@ -60,13 +60,13 @@ export async function SendSiteAnnouncementNotification(
if (maybeGame && maybePlaytype) {
toUserIDs = (
await db["game-stats"].find(
await MONGODB_KILL["game-stats"].find(
{ game: maybeGame, playtype: maybePlaytype },
{ projection: { userID: 1 } },
)
).map((e) => e.userID);
} else {
toUserIDs = (await db.users.find({}, { projection: { id: 1 } })).map((e) => e.id);
toUserIDs = (await MONGODB_KILL.users.find({}, { projection: { id: 1 } })).map((e) => e.id);
}
return BulkSendNotification(title, toUserIDs, {
@@ -1,4 +1,4 @@
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import ResetDBState from "#test-utils/resets";
import t from "tap";
@@ -13,7 +13,7 @@ t.test("#SendNotification", (t) => {
content: { questID: "foo", game: "iidx", playtype: "SP" },
});
const dbRes = await db.notifications.findOne({
const dbRes = await MONGODB_KILL.notifications.findOne({
sentTo: 1,
});
@@ -42,7 +42,7 @@ t.test("#BulkSendNotification", (t) => {
content: { questID: "foo", game: "iidx", playtype: "SP" },
});
const dbRes = await db.notifications.findOne({
const dbRes = await MONGODB_KILL.notifications.findOne({
sentTo: 1,
});
@@ -56,7 +56,7 @@ t.test("#BulkSendNotification", (t) => {
},
});
const dbRes2 = await db.notifications.findOne({
const dbRes2 = await MONGODB_KILL.notifications.findOne({
sentTo: 2,
});
@@ -1,6 +1,6 @@
import type { integer, NotificationBody, NotificationDocument } from "tachi-common";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { Random20Hex } from "#utils/misc";
function ConstructNotificationDoc(
@@ -28,7 +28,7 @@ function ConstructNotificationDoc(
export function SendNotification(title: string, toUserID: integer, body: NotificationBody) {
const notification = ConstructNotificationDoc(title, toUserID, body);
return db.notifications.insert(notification);
return MONGODB_KILL.notifications.insert(notification);
}
/**
@@ -42,5 +42,5 @@ export function BulkSendNotification(
) {
const notifications = toUserIDs.map((e) => ConstructNotificationDoc(title, e, body));
return db.notifications.insert(notifications);
return MONGODB_KILL.notifications.insert(notifications);
}
@@ -9,7 +9,7 @@ import type {
} from "tachi-common";
import { log } from "#lib/log/log.js";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { GetNextCounterValue } from "#utils/db";
import { DedupeArr } from "#utils/misc";
@@ -37,7 +37,7 @@ export async function HandleOrphanQueue<GPT extends GPTString>(
) {
log.debug(`Received orphanqueue request for ${chartName}.`);
const orphanChart = await db["orphan-chart-queue"].findOne(
const orphanChart = await MONGODB_KILL["orphan-chart-queue"].findOne(
{ gptString, ...orphanMatchCriteria },
{
projectID: true,
@@ -47,7 +47,7 @@ export async function HandleOrphanQueue<GPT extends GPTString>(
if (!orphanChart) {
log.debug(`Received unknown chart ${chartName}, orphaning.`);
await db["orphan-chart-queue"].insert({
await MONGODB_KILL["orphan-chart-queue"].insert({
gptString,
chartDoc,
songDoc,
@@ -77,10 +77,10 @@ export async function HandleOrphanQueue<GPT extends GPTString>(
songDoc.id = songID;
chartDoc.songID = songID;
await db.anySongs[game].insert(songDoc);
await db.anyCharts[game].insert(chartDoc);
await MONGODB_KILL.anySongs[game].insert(songDoc);
await MONGODB_KILL.anyCharts[game].insert(chartDoc);
await db["orphan-chart-queue"].remove({
await MONGODB_KILL["orphan-chart-queue"].remove({
_id: orphanChart._id,
});
@@ -90,7 +90,7 @@ export async function HandleOrphanQueue<GPT extends GPTString>(
// otherwise, update the state of this orphan.
log.debug(`UserID ${userID} played ${chartName}, which is now at ${playcount} plays.`);
await db["orphan-chart-queue"].update(
await MONGODB_KILL["orphan-chart-queue"].update(
{
_id: orphanChart._id,
},
@@ -115,7 +115,7 @@ export async function DeorphanIfInQueue<GPT extends GPTString>(
game: GPTStringToGame[GPT],
orphanMatchCriteria: FilterQuery<OrphanChartDocument<GPT>>,
): Promise<ChartDocument<GPT> | null> {
const orphanChart = await db["orphan-chart-queue"].findOne(
const orphanChart = await MONGODB_KILL["orphan-chart-queue"].findOne(
{ gptString, ...orphanMatchCriteria },
{
projectID: true,
@@ -136,9 +136,9 @@ export async function DeorphanIfInQueue<GPT extends GPTString>(
songDoc.id = songID;
chartDoc.songID = songID;
await db.anySongs[game].insert(songDoc);
await db.anyCharts[game].insert(chartDoc);
await db["orphan-chart-queue"].remove({
await MONGODB_KILL.anySongs[game].insert(songDoc);
await MONGODB_KILL.anyCharts[game].insert(chartDoc);
await MONGODB_KILL["orphan-chart-queue"].remove({
_id: orphanChart._id,
});
+10 -10
View File
@@ -4,7 +4,7 @@ import { SetRivalsFailReasons } from "#lib/constants/err-codes";
import { log } from "#lib/log/log.js";
import { SendSetRivalNotification } from "#lib/notifications/notification-wrappers";
import { ServerConfig } from "#lib/setup/config";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { ArrayDiff } from "#utils/misc";
import { GetUsersWithIDs, GetUserWithIDGuaranteed } from "#utils/user";
import {
@@ -21,7 +21,7 @@ import {
* Throws if the user hasn't played the GPT in question.
*/
export async function GetRivalIDs(userID: integer, game: GameGroup, playtype: Playtype) {
const gameSettings = await db["game-settings"].findOne(
const gameSettings = await MONGODB_KILL["game-settings"].findOne(
{
userID,
game,
@@ -63,7 +63,7 @@ export async function GetEveryonesRivalIDs(
game: GameGroup,
playtype: Playtype,
): Promise<Record<number, Array<number>>> {
const allGameSettings = await db["game-settings"].find(
const allGameSettings = await MONGODB_KILL["game-settings"].find(
{
game,
playtype,
@@ -104,7 +104,7 @@ export async function SetRivals(
return SetRivalsFailReasons.RIVALED_SELF;
}
const playedGPTCount = await db["game-settings"].count({
const playedGPTCount = await MONGODB_KILL["game-settings"].count({
userID: { $in: newRivals },
game,
playtype,
@@ -115,7 +115,7 @@ export async function SetRivals(
return SetRivalsFailReasons.RIVALS_HAVENT_PLAYED_GPT;
}
const currentGameSettings = await db["game-settings"].findOne({
const currentGameSettings = await MONGODB_KILL["game-settings"].findOne({
userID,
game,
playtype,
@@ -145,7 +145,7 @@ export async function SetRivals(
newSubs.map((toUserID) => SendSetRivalNotification(toUserID, user, game, playtype)),
);
await db["game-settings"].update(
await MONGODB_KILL["game-settings"].update(
{
userID,
game,
@@ -206,7 +206,7 @@ export async function RemoveRival(
* Get all of the userIDs of people who rival the userID for this GPT.
*/
export async function GetChallengerIDs(userID: integer, game: GameGroup, playtype: Playtype) {
const result = await db["game-settings"].find(
const result = await MONGODB_KILL["game-settings"].find(
{
game,
playtype,
@@ -252,7 +252,7 @@ export async function UpdatePlayersRivalRankings(
const rivalIDs = await GetRivalIDs(userID, game, playtype);
// get all of this user's chartIDs so we know what to update
const userPBs = (await db["personal-bests"].find(
const userPBs = (await MONGODB_KILL["personal-bests"].find(
{ userID, game, playtype },
{ projection: { chartID: 1, [`scoreData.${gptConfig.defaultMetric}`]: 1 } },
)) as Array<{ chartID: string; scoreData: { percent: number } }>;
@@ -262,7 +262,7 @@ export async function UpdatePlayersRivalRankings(
await Promise.all(
userPBs.map(async (pb) => {
const rivalRank =
(await db["personal-bests"].count({
(await MONGODB_KILL["personal-bests"].count({
chartID: pb.chartID,
userID: { $in: rivalIDs },
[`scoreData.${gptConfig.defaultMetric}`]: { $gt: pb.scoreData.percent },
@@ -285,5 +285,5 @@ export async function UpdatePlayersRivalRankings(
return;
}
await db["personal-bests"].bulkWrite(bwrite, { ordered: false });
await MONGODB_KILL["personal-bests"].bulkWrite(bwrite, { ordered: false });
}
@@ -1,7 +1,7 @@
import type { GoalDocument, GoalSubscriptionDocument } from "tachi-common";
import { log } from "#lib/log/log.js";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import { dmf } from "#test-utils/misc.js";
import ResetDBState from "#test-utils/resets";
import {
@@ -51,10 +51,10 @@ t.test("#UpdateGoalsForUser", (t) => {
};
t.test("Should correctly update goals when user achieves goal.", async (t) => {
await db.goals.insert(baseGoalDocument);
await MONGODB_KILL.goals.insert(baseGoalDocument);
await db["goal-subs"].insert(baseGoalSubscriptionDocument);
await db["personal-bests"].insert(TestingIIDXSPScorePB);
await MONGODB_KILL["goal-subs"].insert(baseGoalSubscriptionDocument);
await MONGODB_KILL["personal-bests"].insert(TestingIIDXSPScorePB);
const ugMap = new Map([["FAKE_GOAL_ID", baseGoalSubscriptionDocument]]);
@@ -80,7 +80,7 @@ t.test("#UpdateGoalsForUser", (t) => {
},
]);
const r = await db["goal-subs"].findOne({ goalID: "FAKE_GOAL_ID", userID: 1 });
const r = await MONGODB_KILL["goal-subs"].findOne({ goalID: "FAKE_GOAL_ID", userID: 1 });
t.hasStrict(
r,
@@ -100,19 +100,21 @@ t.test("#UpdateGoalsForUser", (t) => {
t.test("Should correctly update goals when user does not achieve goal.", async (t) => {
const goal = dmf(baseGoalDocument, { criteria: { value: 2 } });
await db.goals.insert(goal);
await MONGODB_KILL.goals.insert(goal);
const goalSub = deepmerge(baseGoalSubscriptionDocument, {
outOf: 2,
outOfHuman: "2",
}) as unknown as GoalSubscriptionDocument;
await db["goal-subs"].insert(goalSub);
await MONGODB_KILL["goal-subs"].insert(goalSub);
// we dont delete _id here because updategoalsforuser
// depends on usergoal _id
await db["personal-bests"].insert(dmf(TestingIIDXSPScorePB, { scoreData: { score: 1 } }));
await MONGODB_KILL["personal-bests"].insert(
dmf(TestingIIDXSPScorePB, { scoreData: { score: 1 } }),
);
const ugMap = new Map([["FAKE_GOAL_ID", goalSub]]);
@@ -138,7 +140,7 @@ t.test("#UpdateGoalsForUser", (t) => {
},
]);
const r = await db["goal-subs"].findOne({ goalID: "FAKE_GOAL_ID", userID: 1 });
const r = await MONGODB_KILL["goal-subs"].findOne({ goalID: "FAKE_GOAL_ID", userID: 1 });
t.hasStrict(
r,
@@ -196,14 +198,14 @@ t.todo("#GetAndUpdateUsersGoals", (t) => {
t.test("#ProcessGoal", (t) => {
t.beforeEach(ResetDBState);
t.beforeEach(async () => {
await db.goals.insert(HC511Goal);
await MONGODB_KILL.goals.insert(HC511Goal);
});
t.test("Should process the users goal if a score has changed.", async (t) => {
await db["goal-subs"].insert(HC511UserGoal);
await MONGODB_KILL["goal-subs"].insert(HC511UserGoal);
// score is EX HARD CLEAR by default.
await db["personal-bests"].insert(TestingIIDXSPScorePB);
await MONGODB_KILL["personal-bests"].insert(TestingIIDXSPScorePB);
const res = await ProcessGoal(HC511Goal, HC511UserGoal, 1, log);
@@ -251,7 +253,7 @@ t.test("#ProcessGoal", (t) => {
userID: 1,
};
await db["goal-subs"].insert(achievedGoalSub);
await MONGODB_KILL["goal-subs"].insert(achievedGoalSub);
const res = await ProcessGoal(HC511Goal, achievedGoalSub, 1, log);
@@ -285,7 +287,7 @@ t.test("#ProcessGoal", (t) => {
});
t.test("Should return undefined if there's no score.", async (t) => {
await db["goal-subs"].insert(HC511UserGoal);
await MONGODB_KILL["goal-subs"].insert(HC511UserGoal);
const res = await ProcessGoal(HC511Goal, HC511UserGoal, 1, log);
@@ -295,17 +297,20 @@ t.test("#ProcessGoal", (t) => {
});
t.test("Should return undefined if the progress has not changed.", async (t) => {
await db["goal-subs"].insert(HC511UserGoal);
await db["personal-bests"].insert(TestingIIDXSPScorePB);
await MONGODB_KILL["goal-subs"].insert(HC511UserGoal);
await MONGODB_KILL["personal-bests"].insert(TestingIIDXSPScorePB);
const firstUpdate = await ProcessGoal(HC511Goal, HC511UserGoal, 1, log);
// ignore this one
t.not(firstUpdate, undefined, "Should NOT return undefined.");
await db["goal-subs"].bulkWrite([firstUpdate!.bwrite]);
await MONGODB_KILL["goal-subs"].bulkWrite([firstUpdate!.bwrite]);
const goalSub = await db["goal-subs"].findOne({ userID: 1, goalID: HC511Goal.goalID });
const goalSub = await MONGODB_KILL["goal-subs"].findOne({
userID: 1,
goalID: HC511Goal.goalID,
});
const secondUpdate = await ProcessGoal(HC511Goal, goalSub!, 1, log);
@@ -3,7 +3,7 @@ import type { GameGroup, GoalDocument, GoalSubscriptionDocument, integer } from
import { EvaluateGoalForUser, GetRelevantGoals } from "#lib/targets/goals";
import { EmitWebhookEvent } from "#lib/webhooks/webhooks";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
/**
* Update a user's progress on all of their set goals.
@@ -90,7 +90,7 @@ export async function UpdateGoalsForUser(
});
}
await db["goal-subs"].bulkWrite(bulkWrite, { ordered: false });
await MONGODB_KILL["goal-subs"].bulkWrite(bulkWrite, { ordered: false });
return importInfo;
}
@@ -209,14 +209,14 @@ export async function ProcessGoal(
}
export async function UpdateGoalsInFolder(folderID: string, log: KtLogger) {
const goals = await db.goals.find({
const goals = await MONGODB_KILL.goals.find({
"charts.type": "folder",
"charts.data": folderID,
});
log.info(`Updating ${goals.length} goals for ${folderID}`);
const goalSubs = await db["goal-subs"].find({
const goalSubs = await MONGODB_KILL["goal-subs"].find({
goalID: { $in: goals.map((e) => e.goalID) },
});
@@ -2,7 +2,7 @@ import type { integer } from "tachi-common";
import { ONE_DAY, ONE_HOUR } from "#lib/constants/time";
import { log } from "#lib/log/log.js";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
/**
* If a user has no ongoing import, enable the import lock and return true.
@@ -13,19 +13,19 @@ import db from "#services/mongo/db";
* has a lock.
*/
export async function CheckAndSetOngoingImportLock(userID: integer) {
const lockExists = await db["import-locks"].findOne({
const lockExists = await MONGODB_KILL["import-locks"].findOne({
userID,
});
if (!lockExists) {
await db["import-locks"].insert({
await MONGODB_KILL["import-locks"].insert({
userID,
locked: false,
lockedAt: null,
});
} else if (lockExists.locked && lockExists.lockedAt! + ONE_DAY < Date.now()) {
log.warn(`Removed import lock for ${userID} as it is ostensibly stuck.`);
await db["import-locks"].update(
await MONGODB_KILL["import-locks"].update(
{
userID,
},
@@ -38,7 +38,7 @@ export async function CheckAndSetOngoingImportLock(userID: integer) {
);
}
const lockWasSet = await db["import-locks"].findOneAndUpdate(
const lockWasSet = await MONGODB_KILL["import-locks"].findOneAndUpdate(
{
userID,
locked: false,
@@ -68,7 +68,7 @@ export async function CheckAndSetOngoingImportLock(userID: integer) {
* Disable a users import lock.
*/
export function UnsetOngoingImportLock(userID: integer) {
return db["import-locks"].findOneAndUpdate(
return MONGODB_KILL["import-locks"].findOneAndUpdate(
{
userID,
locked: true,
@@ -1,7 +1,7 @@
import type { BatchManualScore } from "tachi-common";
import { log } from "#lib/log/log.js";
import db from "#services/mongo/db";
import MONGODB_KILL from "#services/mongo/db";
import ResetDBState from "#test-utils/resets";
import deepmerge from "deepmerge";
import fjsh from "fast-json-stable-hash";
@@ -56,7 +56,7 @@ t.test("#OrphanScore", (t) => {
"Should be a checksum of the orphan's contents.",
);
const dbCheck = await db["orphan-scores"].findOne({
const dbCheck = await MONGODB_KILL["orphan-scores"].findOne({
orphanID: res.orphanID,
});
@@ -101,7 +101,7 @@ t.test("#OrphanScore", (t) => {
t.equal(res2.success, false);
t.equal(res1.orphanID, res2.orphanID);
const dbCheck = await db["orphan-scores"].count();
const dbCheck = await MONGODB_KILL["orphan-scores"].count();
t.equal(dbCheck, 1);
});
@@ -163,7 +163,9 @@ t.test("#ReprocessOrphan", (t) => {
"Should successfully import the score.",
);
const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID });
const orphan = await MONGODB_KILL["orphan-scores"].findOne({
orphanID: orphanDoc.orphanID,
});
t.equal(orphan, null, "Should remove the orphan document.");
@@ -171,7 +173,7 @@ t.test("#ReprocessOrphan", (t) => {
});
t.test("Should delete the orphan doc and return null if the score is invalid.", async (t) => {
await db["orphan-scores"].insert(orphanDoc);
await MONGODB_KILL["orphan-scores"].insert(orphanDoc);
const res = await ReprocessOrphan(
deepmerge(orphanDoc, {
data: {
@@ -184,7 +186,9 @@ t.test("#ReprocessOrphan", (t) => {
t.equal(res, null);
const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID });
const orphan = await MONGODB_KILL["orphan-scores"].findOne({
orphanID: orphanDoc.orphanID,
});
t.equal(orphan, null, "Should remove the orphan document.");
@@ -194,7 +198,7 @@ t.test("#ReprocessOrphan", (t) => {
t.test(
"Should keep the orphan doc and return false if no parents could be found.",
async (t) => {
await db["orphan-scores"].insert(orphanDoc);
await MONGODB_KILL["orphan-scores"].insert(orphanDoc);
const res = await ReprocessOrphan(
deepmerge(orphanDoc, {
data: {
@@ -207,7 +211,9 @@ t.test("#ReprocessOrphan", (t) => {
t.equal(res, false);
const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID });
const orphan = await MONGODB_KILL["orphan-scores"].findOne({
orphanID: orphanDoc.orphanID,
});
t.not(orphan, null, "Should not remove the orphan document.");
@@ -218,12 +224,12 @@ t.test("#ReprocessOrphan", (t) => {
t.test(
"Should keep the orphan doc and return false if an internal failure was triggered.",
async (t) => {
await db["orphan-scores"].insert(orphanDoc);
await MONGODB_KILL["orphan-scores"].insert(orphanDoc);
const GAZER_SHA256 = "195fe1be5c3e74fccd04dc426e05f8a9cfa8a1059c339d0a23e99f63661f0b7d";
// force an internal failure by removing all bms songs and then
// leaving a valid bms chart to match with.
await db.songs.bms.remove({});
await MONGODB_KILL.songs.bms.remove({});
const res = await ReprocessOrphan(
deepmerge(orphanDoc, {
@@ -242,7 +248,9 @@ t.test("#ReprocessOrphan", (t) => {
t.equal(res, false);
const orphan = await db["orphan-scores"].findOne({ orphanID: orphanDoc.orphanID });
const orphan = await MONGODB_KILL["orphan-scores"].findOne({
orphanID: orphanDoc.orphanID,
});
t.not(orphan, null, "Should not remove the orphan document.");

Some files were not shown because too many files have changed in this diff Show More