mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-11 00:38:28 +03:00
more (#53)
* 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:
@@ -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:",
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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.`,
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 } };
|
||||
}
|
||||
|
||||
@@ -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`))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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] });
|
||||
|
||||
@@ -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/**"],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user