* checkpoint

* server compiles again

* fmt

* checkpoint

* last one out, get the lights

* euston station

* like, mostly good. I'm under the ocean right now.

* fix: some nonsense AI stuff

* fix: new eslint rules, and cleanup imports across the board

* feat: folder slug rewrite, email logic change, too

* fix: register must use txn not db

* checkpoint
This commit is contained in:
zk
2026-04-20 23:11:16 +01:00
committed by GitHub
parent edda03e916
commit 2bc4c3a628
1073 changed files with 52259 additions and 56015 deletions
+1 -3
View File
@@ -7,13 +7,11 @@
"license": "MIT",
"scripts": {
"build": "tsgo -b",
"start": "pnpm build && node -r ts-node/register -r tsconfig-paths/register js/main.js",
"start-no-build": "node -r ts-node/register -r tsconfig-paths/register js/main.js",
"lint": "eslint .",
"lint-fix": "eslint . --fix",
"typecheck": "tsgo --noEmit",
"test": "vitest",
"bench": "vitest bench --passWithNoTests"
"bench": "vitest bench"
},
"devDependencies": {
"@types/express": "catalog:",
+1 -1
View File
@@ -21,7 +21,7 @@ export const ActionSignatures = {
session_count: z.number().int(),
error_count: z.number().int(),
user_id: z.number().int(),
game: z.string(),
games: z.string().array(),
}),
},
} satisfies Record<string, ActionSignature>;
+9 -10
View File
@@ -1,4 +1,4 @@
import type { MONGO_ImportDocument } from "tachi-common";
import type { ImportDocument } from "tachi-common";
import db from "#services/pg/db";
import { createTestAccount } from "#test-utils/db";
@@ -13,11 +13,12 @@ vi.mock("../utils/api-requests");
const { PerformScoreImport } = await import("../utils/api-requests");
function makeImportDoc(overrides: Partial<MONGO_ImportDocument> = {}): MONGO_ImportDocument {
function makeImportDoc(overrides: Partial<ImportDocument> = {}): ImportDocument {
return {
importID: "import-abc-123",
userID: 1,
game: "iidx-sp",
gameGroup: "iidx",
games: ["iidx-sp"],
scoreIDs: ["s1", "s2"],
createdSessions: [{ sessionID: "sess1", type: "Created" }],
errors: [],
@@ -25,7 +26,7 @@ function makeImportDoc(overrides: Partial<MONGO_ImportDocument> = {}): MONGO_Imp
goalProgress: [],
questProgress: [],
...overrides,
} as unknown as MONGO_ImportDocument;
} as unknown as ImportDocument;
}
describe("ACTION_Sync", () => {
@@ -50,7 +51,7 @@ describe("ACTION_Sync", () => {
session_count: 1,
error_count: 0,
user_id: 1,
game: "iidx-sp",
games: ["iidx-sp"],
});
});
@@ -82,7 +83,7 @@ describe("ACTION_Sync", () => {
it("throws ExpectedErr when PerformScoreImport returns an error string", async () => {
vi.mocked(PerformScoreImport).mockResolvedValue(
"Your API key has expired." as unknown as MONGO_ImportDocument,
"Your API key has expired." as unknown as ImportDocument,
);
await expect(ACTION_Sync(taker, syncInput)).rejects.toMatchObject({
@@ -93,7 +94,7 @@ describe("ACTION_Sync", () => {
it("the ExpectedErr thrown on failure is an ExpectedErr instance", async () => {
vi.mocked(PerformScoreImport).mockResolvedValue(
"Import failed." as unknown as MONGO_ImportDocument,
"Import failed." as unknown as ImportDocument,
);
await expect(ACTION_Sync(taker, syncInput)).rejects.toBeInstanceOf(ExpectedErr);
@@ -120,9 +121,7 @@ describe("ACTION_Sync", () => {
});
it("writes a BAD action row when PerformScoreImport returns an error string", async () => {
vi.mocked(PerformScoreImport).mockResolvedValue(
"Bad import." as unknown as MONGO_ImportDocument,
);
vi.mocked(PerformScoreImport).mockResolvedValue("Bad import." as unknown as ImportDocument);
await expect(ACTION_Sync(taker, syncInput)).rejects.toBeInstanceOf(ExpectedErr);
+1 -1
View File
@@ -20,7 +20,7 @@ export const ACTION_Sync = MakeAction(
session_count: importDoc.createdSessions.length,
error_count: importDoc.errors.length,
user_id: importDoc.userID,
game: importDoc.game,
games: importDoc.games,
};
},
);
+3 -3
View File
@@ -1,4 +1,4 @@
import type { GameGroup, integer, TachiServerCoreConfig } from "tachi-common";
import type { GameGroup, integer, TachiServerCoreConfig, V3Game } from "tachi-common";
import { log } from "bliss/log";
import { config } from "dotenv";
@@ -49,7 +49,7 @@ export interface ProcessEnvironment {
OAUTH_CLIENT_SECRET: string;
DISCORD_TOKEN: string;
DISCORD_SERVER_ID: string;
DISCORD_GAME_CHANNELS: Partial<Record<GameGroup, string>>;
DISCORD_GAME_CHANNELS: Partial<Record<V3Game, string>>;
DISCORD_ADMIN_USERS: string[];
DISCORD_APPROVED_ROLE: string | undefined;
DISCORD_LIMBO_CHANNEL: string | undefined;
@@ -135,7 +135,7 @@ export const ServerConfig = await GetServerConfig();
// 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) {
for (const game of ServerConfig.GAME_GROUPS) {
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.`,
+3 -3
View File
@@ -1,4 +1,4 @@
import type { MONGO_APITokenDocument, MONGO_UserDocument, WebhookEvents } from "tachi-common";
import type { APITokenDocument, UserDocument, WebhookEvents } from "tachi-common";
import { log } from "#utils/log";
import { HandleQuestAchievedV1 } from "#webhook-handlers/quest-achieved";
@@ -58,7 +58,7 @@ app.get("/oauth/callback", async (req, res) => {
return res.status(400).send("Bad Request.");
}
const tokenRes = await TachiServerV1Request<MONGO_APITokenDocument>(
const tokenRes = await TachiServerV1Request<APITokenDocument>(
RequestTypes.POST,
"/oauth/token",
null,
@@ -84,7 +84,7 @@ app.get("/oauth/callback", async (req, res) => {
const discordID = req.query.context;
const apiToken = tokenRes.body.token!;
const whoamiRes = await TachiServerV1Get<MONGO_UserDocument>("/users/me", apiToken);
const whoamiRes = await TachiServerV1Get<UserDocument>("/users/me", apiToken);
if (!whoamiRes.success) {
log.error({ discordID }, "Failed to request user with token we just got?");
@@ -66,7 +66,7 @@ const command: SlashCommand = {
.addField("Errors", result.error_count.toString(), true)
.addField(
"Your Profile",
`${Env.TACHI_SERVER_LOCATION}/u/${result.user_id}/games/${result.game}`,
`${Env.TACHI_SERVER_LOCATION}/u/${result.user_id}/games/${result.games[0]}`,
);
} catch (e) {
if (ExpectedErr.is(e)) {
+28 -37
View File
@@ -1,19 +1,18 @@
import type { CommandInteraction } from "discord.js";
import type {
GameGroup,
integer,
MONGO_ChartDocument,
MONGO_GoalDocument,
MONGO_ImportDocument,
MONGO_PBScoreDocument,
MONGO_QuestDocument,
MONGO_SongDocument,
MONGO_UserDocument,
Playtype,
} from "tachi-common";
import { Env } from "#config";
import { log } from "#utils/log";
import {
type ChartDocument,
type GoalDocument,
type ImportDocument,
type integer,
type PBScoreDocument,
type QuestDocument,
type SongDocument,
type UserDocument,
type V3Game,
} from "tachi-common";
import type { ImportDeferred, ImportPollStatus, UGPTStats } from "./return-types";
@@ -21,7 +20,7 @@ import { RequestTypes, TachiServerV1Get, TachiServerV1Request } from "./fetch-ta
import { Sleep } from "./misc";
export async function GetUserInfo(userID: string | integer) {
const res = await TachiServerV1Get<MONGO_UserDocument>(`/users/${userID}`, null);
const res = await TachiServerV1Get<UserDocument>(`/users/${userID}`, null);
if (!res.success) {
throw new Error(`Failed to fetch user with userID ${userID}.`);
@@ -30,22 +29,19 @@ export async function GetUserInfo(userID: string | integer) {
return res.body;
}
export async function GetUGPTStats(userID: string | integer, game: GameGroup, playtype: Playtype) {
const res = await TachiServerV1Get<UGPTStats>(
`/users/${userID}/games/${game}/${playtype}`,
null,
);
export async function GetUGPTStats(userID: string | integer, game: V3Game) {
const res = await TachiServerV1Get<UGPTStats>(`/users/${userID}/games/${game}`, null);
if (!res.success) {
throw new Error(`Failed to fetch UGPT stats for userID ${userID}, ${game}, ${playtype}.`);
throw new Error(`Failed to fetch UGPT stats for userID ${userID}, ${game}.`);
}
return res.body;
}
export async function GetGoalWithID(goalID: string, game: GameGroup, playtype: Playtype) {
const res = await TachiServerV1Get<{ goal: MONGO_GoalDocument }>(
`/games/${game}/${playtype}/targets/goals/${goalID}`,
export async function GetGoalWithID(goalID: string, game: V3Game) {
const res = await TachiServerV1Get<{ goal: GoalDocument }>(
`/games/${game}/targets/goals/${goalID}`,
null,
);
@@ -56,9 +52,9 @@ export async function GetGoalWithID(goalID: string, game: GameGroup, playtype: P
return res.body.goal;
}
export async function GetQuestWithID(questID: string, game: GameGroup, playtype: Playtype) {
const res = await TachiServerV1Get<{ quest: MONGO_QuestDocument }>(
`/games/${game}/${playtype}/targets/quests/${questID}`,
export async function GetQuestWithID(questID: string, game: V3Game) {
const res = await TachiServerV1Get<{ quest: QuestDocument }>(
`/games/${game}/targets/quests/${questID}`,
null,
);
@@ -69,14 +65,9 @@ export async function GetQuestWithID(questID: string, game: GameGroup, playtype:
return res.body.quest;
}
export async function GetChartInfoForUser(
userID: string | integer,
chartID: string,
game: GameGroup,
playtype: Playtype,
) {
const res = await TachiServerV1Get<{ chart: MONGO_ChartDocument; song: MONGO_SongDocument }>(
`/games/${game}/${playtype}/charts/${chartID}`,
export async function GetChartInfoForUser(userID: string | integer, chartID: string, game: V3Game) {
const res = await TachiServerV1Get<{ chart: ChartDocument; song: SongDocument }>(
`/games/${game}/charts/${chartID}`,
null,
);
@@ -84,8 +75,8 @@ export async function GetChartInfoForUser(
throw new Error(`Failed to fetch song/chart with chartID ${chartID}.`);
}
const pbRes = await TachiServerV1Get<{ chart: MONGO_ChartDocument; pb: MONGO_PBScoreDocument }>(
`/users/${userID}/games/${game}/${playtype}/pbs/${chartID}`,
const pbRes = await TachiServerV1Get<{ chart: ChartDocument; pb: PBScoreDocument }>(
`/users/${userID}/games/${game}/pbs/${chartID}`,
null,
);
@@ -104,7 +95,7 @@ export async function PerformScoreImport(
body: Record<string, unknown>,
interaction?: CommandInteraction,
) {
const initRes = await TachiServerV1Request<ImportDeferred | MONGO_ImportDocument>(
const initRes = await TachiServerV1Request<ImportDeferred | ImportDocument>(
RequestTypes.POST,
url,
authToken,
@@ -122,7 +113,7 @@ export async function PerformScoreImport(
// this server does not defer imports to a scorequeue
if (initRes.statusCode === 200) {
const result = initRes.body as MONGO_ImportDocument;
const result = initRes.body as ImportDocument;
return result;
} else if (initRes.statusCode === 202) {
+10 -8
View File
@@ -1,23 +1,21 @@
import type { CommandInteraction } from "discord.js";
import type { PrivDiscordUserMap } from "tachi-db";
import { type GameGroup, type MONGO_UserDocument, type Playtype } from "tachi-common";
import { IsValidGame, type UserDocument, type V3Game } from "tachi-common";
import type { Emittable } from "../slash-commands/types";
import { GetUserInfo } from "./api-requests";
import { ParseGPT } from "./misc";
/**
* Utility parser for getting the game, playtype and requesting user, since this is
* Utility parser for getting the game and requesting user, since this is
* a common pattern in the bot.
*/
export async function GetGPTAndUser(
export async function GetGameAndUser(
interaction: CommandInteraction,
requestingUser: PrivDiscordUserMap,
): Promise<
| { content: { game: GameGroup; playtype: Playtype; userDoc: MONGO_UserDocument }; error: null }
| { error: Emittable }
{ content: { game: V3Game; userDoc: UserDocument }; error: null } | { error: Emittable }
> {
const userID = interaction.options.getString("other_user") ?? requestingUser.user_id.toString();
@@ -33,7 +31,11 @@ export async function GetGPTAndUser(
return { error: `This user does not exist.` };
}
const { game, playtype } = ParseGPT(interaction.options.getString("game", true));
const game = interaction.options.getString("game", true);
return { error: null, content: { userDoc, game, playtype } };
if (!IsValidGame(game)) {
return { error: `Invalid game: ${game}.` };
}
return { error: null, content: { userDoc, game } };
}
+4 -4
View File
@@ -1,4 +1,4 @@
import type { integer, MONGO_ImportDocument, MONGO_UserDocument } from "tachi-common";
import type { ImportDocument, integer, UserDocument } from "tachi-common";
import { MessageEmbed } from "discord.js";
@@ -18,7 +18,7 @@ export function CreateEmbed(userID?: integer) {
return embed;
}
export function CreateImportEmbed(importDoc: MONGO_ImportDocument) {
export function CreateImportEmbed(importDoc: ImportDocument) {
return CreateEmbed()
.setTitle(
`Imported ${importDoc.scoreIDs.length} ${Pluralise(
@@ -30,11 +30,11 @@ export function CreateImportEmbed(importDoc: MONGO_ImportDocument) {
.addField("Errors", importDoc.errors.length.toString(), true)
.addField(
"Your Profile",
`${Env.TACHI_SERVER_LOCATION}/u/${importDoc.userID}/games/${importDoc.game}`,
`${Env.TACHI_SERVER_LOCATION}/u/${importDoc.userID}/games/${importDoc.games[0]}`,
);
}
export function CreateUserEmbed(userDoc: MONGO_UserDocument) {
export function CreateUserEmbed(userDoc: UserDocument) {
return CreateEmbed()
.setTitle(`${userDoc.username} (ID: ${userDoc.id})`)
.setThumbnail(PrependTachiUrl(`/users/${userDoc.id}/pfp`))
+10 -43
View File
@@ -4,15 +4,11 @@ import type { ClassInfo } from "tachi-common/types/game-config-utils";
import _ from "lodash";
import { DateTime } from "luxon";
import {
type ChartDocument,
type Classes,
type GameGroup,
type GameGroupConfig,
GetGameGroupConfig,
GetGamePTConfig,
type GPTString,
GetGameConfig,
type integer,
type MONGO_ChartDocument,
type Playtype,
type V3Game,
} from "tachi-common";
import { Env } from "../config";
@@ -40,22 +36,6 @@ export function Entries<K extends string, V>(rec: Partial<Record<K, V>>): Array<
return Object.entries(rec) as Array<[K, V]>;
}
export function ParseGPT(str: string) {
const spl = str.split(":");
const game = spl[0] as GameGroup;
const playtype = spl[1] as Playtype;
// this is an interesting way of game checking noah
const gameConfig = GetGameGroupConfig(game) as GameGroupConfig | undefined;
if (!gameConfig?.playtypes.includes(playtype)) {
throw new Error(`Invalid GPT Combination '${str}'.`);
}
return { game, playtype };
}
export function UppercaseFirst(str: string) {
if (!str[0]) {
return "";
@@ -72,24 +52,17 @@ export function FormatDate(ms: number) {
return DateTime.fromMillis(ms).toLocaleString(DateTime.DATE_HUGE);
}
export function FormatClass(
game: GameGroup,
playtype: Playtype,
classSet: Classes[GPTString],
classValue: string,
) {
const gptConfig = GetGamePTConfig(game, playtype);
export function FormatClass(game: V3Game, classSet: Classes[V3Game], classValue: string) {
const gameConfig = GetGameConfig(game);
// @ts-expect-error hacky access
const classInfo: ClassInfo = gptConfig.classes[classSet]?.values?.find?.(
const classInfo: ClassInfo = gameConfig.classes[classSet]?.values?.find?.(
(k) => k.id === classValue,
);
if (!classInfo) {
throw new Error(
`Couldn't find a class at index ${classValue} for ${game} ${playtype} ${classSet}?`,
);
throw new Error(`Couldn't find a class at index ${classValue} for ${game} ${classSet}?`);
}
return classInfo.display as string;
@@ -98,7 +71,7 @@ export function FormatClass(
/**
* Given a game, return the discord channel it's associated with.
*/
export function GetGameChannel(client: Client, game: GameGroup) {
export function GetGameChannel(client: Client, game: V3Game) {
const gameChannelID = Env.DISCORD_GAME_CHANNELS[game];
if (!gameChannelID) {
@@ -145,14 +118,8 @@ export function GetLimboChannel(client: Client) {
/**
* Given a chart and a game, return a link to the site for that chart.
*/
export function CreateChartLink(chart: MONGO_ChartDocument, game: GameGroup) {
if (chart.isPrimary) {
return `${Env.TACHI_SERVER_LOCATION}/games/${game}/${chart.playtype}/songs/${
chart.songID
}/${encodeURIComponent(chart.difficulty)}`;
}
return `${Env.TACHI_SERVER_LOCATION}/games/${game}/${chart.playtype}/songs/${chart.songID}/${chart.chartID}`;
export function CreateChartLink(chart: ChartDocument) {
return `${Env.TACHI_SERVER_LOCATION}/games/${chart.game}/charts/${chart.chartID}`;
}
export function ConvertInputIntoGenerousRegex(input: string) {
+9 -9
View File
@@ -1,25 +1,25 @@
import type { SlashCommandStringOption } from "@discordjs/builders";
import { FormatGameGroup, GetGameGroupConfig } from "tachi-common";
import { FormatGame, GetGameGroupConfig } from "tachi-common";
import { ServerConfig } from "../config";
/**
* Game Playtype options. Frequently used by things that might need
* Game options. Frequently used by things that might need
* game specific listening.
*/
const GPTChoices: Array<[string, string]> = [];
const GameChoices: Array<[string, string]> = [];
for (const game of ServerConfig.GAMES) {
const gameConfig = GetGameGroupConfig(game);
for (const gameGroup of ServerConfig.GAME_GROUPS) {
const gameGroupConfig = GetGameGroupConfig(gameGroup);
for (const playtype of gameConfig.playtypes) {
GPTChoices.push([FormatGameGroup(game, playtype), `${game}:${playtype}`]);
for (const game of gameGroupConfig.games) {
GameChoices.push([FormatGame(game), game]);
}
}
export const GPTOptions = (str: SlashCommandStringOption) =>
str.setName("game").setDescription("Pick the relevant game.").addChoices(GPTChoices);
export const GameOptions = (str: SlashCommandStringOption) =>
str.setName("game").setDescription("Pick the relevant game.").addChoices(GameChoices);
export function MakeRequired(fn: (str: SlashCommandStringOption) => SlashCommandStringOption) {
return (str: SlashCommandStringOption) => fn(str).setRequired(true);
+14 -14
View File
@@ -1,13 +1,13 @@
import type {
APIPermissions,
GPTString,
ChartDocument,
ImportDocument,
integer,
MONGO_ChartDocument,
MONGO_ImportDocument,
MONGO_ScoreDocument,
MONGO_SongDocument,
MONGO_UserGameStats,
ProfileRatingAlgorithms,
ScoreDocument,
SongDocument,
UserGameStats,
V3Game,
} from "tachi-common";
export interface ServerStatus {
@@ -25,7 +25,7 @@ export interface ImportDeferred {
export type ImportPollStatus =
| {
import: MONGO_ImportDocument;
import: ImportDocument;
importStatus: "completed";
}
| {
@@ -36,15 +36,15 @@ export type ImportPollStatus =
};
};
export interface UGPTStats<GPT extends GPTString = GPTString> {
gameStats: MONGO_UserGameStats;
firstScore: MONGO_ScoreDocument;
mostRecentScore: MONGO_ScoreDocument;
export interface UGPTStats<TGame extends V3Game = V3Game> {
gameStats: UserGameStats;
firstScore: ScoreDocument;
mostRecentScore: ScoreDocument;
totalScores: integer;
rankingData: Record<ProfileRatingAlgorithms[GPT], { outOf: integer; ranking: integer }>;
rankingData: Record<ProfileRatingAlgorithms[TGame], { outOf: integer; ranking: integer }>;
}
export interface ChartQueryReturns {
charts: Array<{ __playcount: integer } & MONGO_ChartDocument>;
songs: Array<MONGO_SongDocument>;
charts: Array<{ __playcount: integer } & ChartDocument>;
songs: Array<SongDocument>;
}
@@ -1,12 +1,10 @@
import { log } from "#utils/log";
import {
type Classes,
FormatGameGroup,
type GameGroup,
GetGamePTConfig,
type GPTString,
FormatGame,
GetGameConfig,
type integer,
type Playtype,
type V3Game,
type WebhookEventClassUpdateV1,
} from "tachi-common";
@@ -20,7 +18,7 @@ import { FormatClass, GetGameChannel } from "../utils/misc";
export async function HandleClassUpdateV1(
event: WebhookEventClassUpdateV1["content"],
): Promise<integer> {
const { game, playtype } = event;
const { game } = event;
let channel;
@@ -33,7 +31,7 @@ export async function HandleClassUpdateV1(
return 500;
}
if (!ShouldRenderUpdate(game, playtype, event.set, event.new)) {
if (!ShouldRenderUpdate(game, event.set, event.new)) {
log.info(
`Not rendering class update ${event.set}: ${event.old} -> ${event.new} (not relevant).`,
);
@@ -42,10 +40,10 @@ export async function HandleClassUpdateV1(
const userDoc = await GetUserInfo(event.userID);
const minimumNecessaryScores = GetMinimumScores(game, playtype, event.set);
const minimumNecessaryScores = GetMinimumScores(game, event.set);
if (minimumNecessaryScores !== null) {
const { totalScores } = await GetUGPTStats(userDoc.id, game, playtype);
const { totalScores } = await GetUGPTStats(userDoc.id, game);
// Do not render if the user hasn't hit the score cap.
if (totalScores < minimumNecessaryScores) {
@@ -56,19 +54,15 @@ export async function HandleClassUpdateV1(
}
}
const newClass = FormatClass(game, playtype, event.set, event.new);
const newClass = FormatClass(game, event.set, event.new);
const embed = CreateEmbed()
.setTitle(
`${userDoc.username} just achieved ${newClass} in ${FormatGameGroup(game, playtype)}!`,
)
.setURL(`${Env.TACHI_SERVER_LOCATION}/users/${userDoc.username}/games/${game}/${playtype}`)
.setTitle(`${userDoc.username} just achieved ${newClass} in ${FormatGame(game)}!`)
.setURL(`${Env.TACHI_SERVER_LOCATION}/u/${userDoc.username}/games/${game}`)
.setThumbnail(PrependTachiUrl(`/users/${userDoc.id}/pfp`));
if (event.old !== null) {
embed.setDescription(
`(This was raised from ${FormatClass(game, playtype, event.set, event.old)}.)`,
);
embed.setDescription(`(This was raised from ${FormatClass(game, event.set, event.old)}.)`);
}
await channel.send({ embeds: [embed] });
@@ -79,17 +73,12 @@ export async function HandleClassUpdateV1(
/**
* Returns Whether this class update is notable enough to be rendered or not.
*/
function ShouldRenderUpdate(
game: GameGroup,
playtype: Playtype,
classSet: Classes[GPTString],
classValue: string,
) {
const config = GetGamePTConfig(game, playtype);
function ShouldRenderUpdate(game: V3Game, classSet: Classes[V3Game], classValue: string) {
const config = GetGameConfig(game);
const classSpec = config.classes[classSet];
if (classSpec === undefined) {
log.error(`Invalid class ${classSet} for ${game} ${playtype}`);
log.error(`Invalid class ${classSet} for ${game}`);
return false;
}
@@ -103,28 +92,24 @@ function ShouldRenderUpdate(
const minimumId = ids.indexOf(classSpec.minimumRelevantValue);
if (currentId < 0) {
log.error(`Invalid classValue ${classValue} for ${game} ${playtype}`);
log.error(`Invalid classValue ${classValue} for ${game}`);
return false;
}
if (minimumId < 0) {
log.error(`Invalid minimum classValue ${classValue} for ${game} ${playtype}`);
log.error(`Invalid minimum classValue ${classValue} for ${game}`);
return false;
}
return currentId >= minimumId;
}
function GetMinimumScores(
game: GameGroup,
playtype: Playtype,
classSet: Classes[GPTString],
): integer | null {
const config = GetGamePTConfig(game, playtype);
function GetMinimumScores(game: V3Game, classSet: Classes[V3Game]): integer | null {
const config = GetGameConfig(game);
const classSpec = config.classes[classSet];
if (classSpec === undefined) {
log.error(`Invalid class ${classSet} for ${game} ${playtype}`);
log.error(`Invalid class ${classSet} for ${game}`);
return null;
}
@@ -2,9 +2,9 @@ import { Env } from "#config";
import { PrependTachiUrl } from "#utils/fetch-tachi";
import { log } from "#utils/log";
import {
GetGameGroupConfig,
FormatGame,
type GoalDocument,
type integer,
type MONGO_GoalDocument,
type WebhookEventGoalAchievedV1,
} from "tachi-common";
@@ -32,19 +32,15 @@ export async function HandleGoalAchievedV1(
const userDoc = await GetUserInfo(event.userID);
const goalDocuments = await Promise.all(
event.goals.map((e) => GetGoalWithID(e.goalID, game, e.playtype)),
event.goals.map((e) => GetGoalWithID(e.goalID, e.game)),
);
const goalMap = new Map<string, MONGO_GoalDocument>();
const goalMap = new Map<string, GoalDocument>();
for (const goalDoc of goalDocuments) {
goalMap.set(goalDoc.goalID, goalDoc);
}
const gameConfig = GetGameGroupConfig(game);
const shouldShowPlaytype = gameConfig.playtypes.length !== 1;
const embed = CreateEmbed(userDoc.id)
.setTitle(
`${userDoc.username} just achieved ${event.goals.length} ${Pluralise(
@@ -66,7 +62,7 @@ export async function HandleGoalAchievedV1(
: `${e.old.progressHuman}/${e.old.outOfHuman} -> ${e.new.progressHuman}/${e.new.outOfHuman}`;
return {
name: `${goal.name}${shouldShowPlaytype ? ` (${e.playtype})` : ""}`,
name: `${goal.name} (${FormatGame(e.game)})`,
value,
};
}),
@@ -1,7 +1,7 @@
import { Env } from "#config";
import { PrependTachiUrl } from "#utils/fetch-tachi";
import { log } from "#utils/log";
import { GetGameGroupConfig, type integer, type WebhookEventQuestAchievedV1 } from "tachi-common";
import { FormatGame, type integer, type WebhookEventQuestAchievedV1 } from "tachi-common";
import { client } from "../main";
import { GetQuestWithID, GetUserInfo } from "../utils/api-requests";
@@ -11,7 +11,7 @@ import { GetGameChannel } from "../utils/misc";
export async function HandleQuestAchievedV1(
event: WebhookEventQuestAchievedV1["content"],
): Promise<integer> {
const { game, playtype } = event;
const { game } = event;
let channel;
@@ -26,16 +26,13 @@ export async function HandleQuestAchievedV1(
const userDoc = await GetUserInfo(event.userID);
const quest = await GetQuestWithID(event.questID, game, playtype);
const gameConfig = GetGameGroupConfig(game);
const shouldShowPlaytype = gameConfig.playtypes.length > 1 ? ` (${playtype})` : "";
const quest = await GetQuestWithID(event.questID, game);
const embed = CreateEmbed(userDoc.id)
.setThumbnail(PrependTachiUrl(`/users/${userDoc.id}/pfp`))
.setURL(`${Env.TACHI_SERVER_LOCATION}/u/${userDoc.username}`)
.setTitle(
`${userDoc.username} just completed the ${quest.name}${shouldShowPlaytype} quest!`,
`${userDoc.username} just completed the ${quest.name} (${FormatGame(game)}) quest!`,
);
await channel.send({ embeds: [embed] });
+4
View File
@@ -17,6 +17,8 @@ export default defineConfig({
},
test: {
passWithNoTests: true,
// Static env vars. POSTGRES_URL is set dynamically per-worker in vitest.setup.ts
// so each worker gets its own isolated database.
env: {
@@ -38,7 +40,9 @@ export default defineConfig({
pool: "forks",
coverage: {
enabled: true,
provider: "v8",
reporter: ["text", "html", "clover", "json", "lcov"],
include: ["src/actions/**"],
},
},
+5 -4
View File
@@ -11,6 +11,7 @@
*/
import crypto from "node:crypto";
import { allImportTypes } from "tachi-common/constants/import-types";
const WORKER_ID = crypto.randomUUID().slice(0, 8);
const WORKER_DB_NAME = `tachi_bot_test_${WORKER_ID}`;
@@ -35,10 +36,10 @@ globalThis.fetch = (url: string | Request | URL): Promise<Response> => {
success: true,
body: {
NAME: "Test Tachi",
GAMES: [],
VERSIONS: {},
SUPPORTS_OAUTH2: false,
OAUTH_CLIENT_ID: null,
TYPE: "boku",
SIGNUPS_ENABLED: true,
GAME_GROUPS: ["iidx"],
IMPORT_TYPES: [...allImportTypes],
},
}),
} as unknown as Response);