mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-30 19:07:58 +03:00
feat: i'm worried about this one
This commit is contained in:
@@ -1,17 +1,20 @@
|
||||
import type { GameGroup, integer, Playtype } from "tachi-common";
|
||||
import { GamePTToV3 } from "tachi-common";
|
||||
|
||||
import { log } from "#lib/log/log";
|
||||
import MONGODB_KILL from "#services/mongo/db";
|
||||
import DB from "#services/pg/db";
|
||||
|
||||
/**
|
||||
* Create GameSettings for a UGPT (which contains their preferences).
|
||||
*/
|
||||
export async function CreateGameSettings(userID: integer, game: GameGroup, playtype: Playtype) {
|
||||
const exists = await MONGODB_KILL["game-settings"].findOne({
|
||||
userID,
|
||||
game,
|
||||
playtype,
|
||||
});
|
||||
const v3Game = GamePTToV3(game, playtype);
|
||||
|
||||
const exists = await DB.selectFrom("game_settings")
|
||||
.select("user_id")
|
||||
.where("user_id", "=", userID)
|
||||
.where("game", "=", v3Game)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (exists) {
|
||||
log.error(
|
||||
@@ -23,31 +26,27 @@ export async function CreateGameSettings(userID: integer, game: GameGroup, playt
|
||||
);
|
||||
}
|
||||
|
||||
let gameSpecific = {};
|
||||
const gameSpecific =
|
||||
game === "iidx"
|
||||
? {
|
||||
display2DXTra: false,
|
||||
bpiTarget: 0,
|
||||
}
|
||||
: {};
|
||||
|
||||
if (game === "iidx") {
|
||||
gameSpecific = {
|
||||
display2DXTra: false,
|
||||
bpiTarget: 0,
|
||||
};
|
||||
}
|
||||
|
||||
await MONGODB_KILL["game-settings"].insert({
|
||||
userID,
|
||||
game,
|
||||
playtype,
|
||||
preferences: {
|
||||
preferredProfileAlg: null,
|
||||
preferredSessionAlg: null,
|
||||
preferredScoreAlg: null,
|
||||
preferredDefaultEnum: null,
|
||||
defaultTable: null,
|
||||
preferredRanking: null,
|
||||
stats: [],
|
||||
gameSpecific,
|
||||
},
|
||||
rivals: [],
|
||||
});
|
||||
await DB.insertInto("game_settings")
|
||||
.values({
|
||||
data: JSON.stringify(gameSpecific),
|
||||
game: v3Game,
|
||||
pf_default_table: null,
|
||||
pf_preferred_default_enum: null,
|
||||
pf_preferred_profile_alg: null,
|
||||
pf_preferred_ranking: null,
|
||||
pf_preferred_score_alg: null,
|
||||
pf_preferred_session_alg: null,
|
||||
user_id: userID,
|
||||
})
|
||||
.execute();
|
||||
|
||||
log.info(`Created game settings for ${userID} (${game} ${playtype}).`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import DB from "#services/pg/db";
|
||||
import { seedUser } from "#test-utils/pg-fixtures";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { UpdateClassIfGreater } from "./class";
|
||||
|
||||
function asClassesJson(v: unknown): { dan?: string } {
|
||||
if (typeof v === "string") {
|
||||
return JSON.parse(v) as { dan?: string };
|
||||
}
|
||||
|
||||
return v as { dan?: string };
|
||||
}
|
||||
|
||||
describe("UpdateClassIfGreater (Postgres)", () => {
|
||||
it("returns false when new class is not greater", async () => {
|
||||
const { id } = await seedUser({ username: `cls_down_${Date.now()}` });
|
||||
|
||||
await DB.insertInto("game_profile")
|
||||
.values({
|
||||
classes: JSON.stringify({ dan: "DAN_1" }),
|
||||
game: "iidx-sp",
|
||||
ratings: JSON.stringify({}),
|
||||
user_id: id,
|
||||
})
|
||||
.execute();
|
||||
|
||||
const result = await UpdateClassIfGreater(id, "iidx", "SP", "dan", "KYU_7");
|
||||
expect(result).toBe(false);
|
||||
|
||||
const row = await DB.selectFrom("game_profile")
|
||||
.select("classes")
|
||||
.where("user_id", "=", id)
|
||||
.where("game", "=", "iidx-sp")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(asClassesJson(row.classes).dan).toBe("DAN_1");
|
||||
});
|
||||
|
||||
it("returns true and updates when new class is greater", async () => {
|
||||
const { id } = await seedUser({ username: `cls_up_${Date.now()}` });
|
||||
|
||||
await DB.insertInto("game_profile")
|
||||
.values({
|
||||
classes: JSON.stringify({ dan: "KYU_7" }),
|
||||
game: "iidx-sp",
|
||||
ratings: JSON.stringify({}),
|
||||
user_id: id,
|
||||
})
|
||||
.execute();
|
||||
|
||||
const result = await UpdateClassIfGreater(id, "iidx", "SP", "dan", "DAN_1");
|
||||
expect(result).toBe(true);
|
||||
|
||||
const row = await DB.selectFrom("game_profile")
|
||||
.select("classes")
|
||||
.where("user_id", "=", id)
|
||||
.where("game", "=", "iidx-sp")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(asClassesJson(row.classes).dan).toBe("DAN_1");
|
||||
|
||||
const ach = await DB.selectFrom("class_achievement")
|
||||
.select(["class_prev_value", "class_value"])
|
||||
.where("user_id", "=", id)
|
||||
.where("game", "=", "iidx-sp")
|
||||
.orderBy("timestamp", "desc")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(ach.class_value).toBe("DAN_1");
|
||||
expect(ach.class_prev_value).toBe("KYU_7");
|
||||
});
|
||||
|
||||
it("creates game_profile and game_settings when none exist (first class)", async () => {
|
||||
const { id } = await seedUser({ username: `cls_new_${Date.now()}` });
|
||||
|
||||
const result = await UpdateClassIfGreater(id, "iidx", "SP", "dan", "DAN_1");
|
||||
expect(result).toBe(null);
|
||||
|
||||
const profile = await DB.selectFrom("game_profile")
|
||||
.selectAll()
|
||||
.where("user_id", "=", id)
|
||||
.where("game", "=", "iidx-sp")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(asClassesJson(profile.classes).dan).toBe("DAN_1");
|
||||
|
||||
const settings = await DB.selectFrom("game_settings")
|
||||
.selectAll()
|
||||
.where("user_id", "=", id)
|
||||
.where("game", "=", "iidx-sp")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
const data =
|
||||
typeof settings.data === "string"
|
||||
? (JSON.parse(settings.data) as { bpiTarget?: number; display2DXTra?: boolean })
|
||||
: (settings.data as { bpiTarget?: number; display2DXTra?: boolean });
|
||||
expect(data.display2DXTra).toBe(false);
|
||||
expect(data.bpiTarget).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CreateGameSettings } from "#lib/game-settings/create-game-settings";
|
||||
import { log } from "#lib/log/log";
|
||||
import { EmitWebhookEvent } from "#lib/webhooks/webhooks";
|
||||
import MONGODB_KILL from "#services/mongo/db";
|
||||
import DB from "#services/pg/db";
|
||||
import {
|
||||
type Classes,
|
||||
type GameGroup,
|
||||
@@ -11,8 +11,42 @@ import {
|
||||
type integer,
|
||||
type MONGO_UserGameStats,
|
||||
type Playtype,
|
||||
GamePTToV3,
|
||||
} from "tachi-common";
|
||||
|
||||
function parseProfileJson<T>(v: unknown): T {
|
||||
if (typeof v === "string") {
|
||||
return JSON.parse(v) as T;
|
||||
}
|
||||
|
||||
return v as T;
|
||||
}
|
||||
|
||||
async function loadUserGameStats(
|
||||
userID: integer,
|
||||
game: GameGroup,
|
||||
playtype: Playtype,
|
||||
): Promise<MONGO_UserGameStats | null> {
|
||||
const v3Game = GamePTToV3(game, playtype);
|
||||
const row = await DB.selectFrom("game_profile")
|
||||
.select(["ratings", "classes"])
|
||||
.where("user_id", "=", userID)
|
||||
.where("game", "=", v3Game)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
userID,
|
||||
game,
|
||||
playtype,
|
||||
ratings: parseProfileJson(row.ratings),
|
||||
classes: parseProfileJson(row.classes),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provided class if it is greater than the one in userGameStats
|
||||
* @returns The provided class if it is greater, NULL if there is nothing
|
||||
@@ -100,8 +134,9 @@ export async function UpdateClassIfGreater(
|
||||
classVal: string,
|
||||
) {
|
||||
const gptString = GetGPTString(game, playtype);
|
||||
const v3Game = GamePTToV3(game, playtype);
|
||||
|
||||
const userGameStats = await MONGODB_KILL["game-stats"].findOne({ userID, game, playtype });
|
||||
const userGameStats = await loadUserGameStats(userID, game, playtype);
|
||||
const isGreater = ReturnClassIfGreater(gptString, classSet, classVal, userGameStats);
|
||||
|
||||
if (isGreater === false) {
|
||||
@@ -109,37 +144,46 @@ export async function UpdateClassIfGreater(
|
||||
}
|
||||
|
||||
if (userGameStats) {
|
||||
await MONGODB_KILL["game-stats"].update(
|
||||
{ userID, game, playtype },
|
||||
{ $set: { [`classes.${classSet}`]: classVal } },
|
||||
);
|
||||
const nextClasses = {
|
||||
...userGameStats.classes,
|
||||
[classSet]: classVal,
|
||||
};
|
||||
|
||||
await DB.updateTable("game_profile")
|
||||
.set({ classes: JSON.stringify(nextClasses) })
|
||||
.where("user_id", "=", userID)
|
||||
.where("game", "=", v3Game)
|
||||
.execute();
|
||||
} else {
|
||||
// insert new game stats for this user - this is an awkward place
|
||||
// to call this - maybe we should call it elsewhere.
|
||||
await MONGODB_KILL["game-stats"].insert({
|
||||
userID,
|
||||
game,
|
||||
playtype,
|
||||
ratings: {},
|
||||
classes: {
|
||||
[classSet]: classVal,
|
||||
},
|
||||
});
|
||||
await DB.insertInto("game_profile")
|
||||
.values({
|
||||
classes: JSON.stringify({ [classSet]: classVal }),
|
||||
game: v3Game,
|
||||
ratings: JSON.stringify({}),
|
||||
user_id: userID,
|
||||
})
|
||||
.execute();
|
||||
|
||||
log.info(`Created new player gamestats for ${userID} (${game} ${playtype})`);
|
||||
|
||||
await CreateGameSettings(userID, game, playtype);
|
||||
}
|
||||
|
||||
await MONGODB_KILL["class-achievements"].insert({
|
||||
game,
|
||||
playtype,
|
||||
userID,
|
||||
classOldValue: isGreater === null ? null : userGameStats!.classes[classSet]!,
|
||||
classSet,
|
||||
classValue: classVal,
|
||||
timeAchieved: Date.now(),
|
||||
});
|
||||
const prevForAchievement =
|
||||
isGreater === null
|
||||
? ""
|
||||
: String(userGameStats?.classes[classSet as keyof typeof userGameStats.classes] ?? "");
|
||||
|
||||
await DB.insertInto("class_achievement")
|
||||
.values({
|
||||
class_prev_value: prevForAchievement,
|
||||
class_set: classSet,
|
||||
class_value: classVal,
|
||||
game: v3Game,
|
||||
timestamp: new Date().toISOString(),
|
||||
user_id: userID,
|
||||
})
|
||||
.execute();
|
||||
|
||||
if (isGreater === null) {
|
||||
void EmitWebhookEvent({
|
||||
|
||||
@@ -145,7 +145,7 @@ export async function FindBMSChartOnHash(hash: string) {
|
||||
|
||||
return chartJoinedToDocument(
|
||||
row as { song_legacy_id: number } & ChartJoinedRow,
|
||||
) as Promise<MONGO_ChartDocument<"bms:7K" | "bms:14K"> | null>;
|
||||
) as MONGO_ChartDocument<"bms:7K" | "bms:14K">;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,12 @@ import DB from "#services/pg/db";
|
||||
import { seedUser } from "#test-utils/pg-fixtures";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { GetAllRankings, GetUsersRankingAndOutOf } from "./user";
|
||||
import {
|
||||
GetAllRankings,
|
||||
GetAllUserRivals,
|
||||
GetUsersRanking,
|
||||
GetUsersRankingAndOutOf,
|
||||
} from "./user";
|
||||
|
||||
// ─── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -155,6 +160,41 @@ describe("GetUsersRankingAndOutOf", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("GetUsersRanking", () => {
|
||||
it("matches GetUsersRankingAndOutOf().ranking", async () => {
|
||||
const { id } = await seedUser({ username: `gr_${Date.now()}` });
|
||||
await seedGameStats(id, 12);
|
||||
|
||||
const stats = makeStats(id, 12);
|
||||
const r = await GetUsersRanking(stats);
|
||||
const full = await GetUsersRankingAndOutOf(stats);
|
||||
|
||||
expect(r).toBe(full.ranking);
|
||||
});
|
||||
});
|
||||
|
||||
describe("GetAllUserRivals", () => {
|
||||
it("returns distinct rival ids from game_rival", async () => {
|
||||
const t = Date.now();
|
||||
const { id: me } = await seedUser({ username: `me_${t}` });
|
||||
const { id: rivalA } = await seedUser({ username: `rv_a_${t}` });
|
||||
const { id: rivalB } = await seedUser({ username: `rv_b_${t}` });
|
||||
|
||||
await DB.insertInto("game_rival")
|
||||
.values({ game: "iidx-sp", rival: rivalA, user_id: me })
|
||||
.execute();
|
||||
await DB.insertInto("game_rival")
|
||||
.values({ game: "sdvx", rival: rivalB, user_id: me })
|
||||
.execute();
|
||||
await DB.insertInto("game_rival")
|
||||
.values({ game: "sdvx", rival: rivalA, user_id: me })
|
||||
.execute();
|
||||
|
||||
const rivals = await GetAllUserRivals(me);
|
||||
expect(rivals.sort((a, b) => a - b)).toEqual([rivalA, rivalB].sort((a, b) => a - b));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── GetAllRankings ───────────────────────────────────────────────────────────
|
||||
|
||||
describe("GetAllRankings", () => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ONE_DAY } from "#lib/constants/time";
|
||||
import { SELECT_USER, ToUserDocument } from "#lib/db-formats/user";
|
||||
import { SELECT_USER_SETTINGS, ToUserSettingsDocument } from "#lib/db-formats/user-settings";
|
||||
import { log } from "#lib/log/log";
|
||||
import MONGODB_KILL from "#services/mongo/db";
|
||||
import DB from "#services/pg/db";
|
||||
import { ISO8601ToUnixMilliseconds } from "#utils/time";
|
||||
import { type Kysely, sql, type Transaction } from "kysely";
|
||||
@@ -161,37 +160,8 @@ export function FormatUserDoc(userdoc: MONGO_UserDocument) {
|
||||
}
|
||||
|
||||
export async function GetUsersRanking(stats: MONGO_UserGameStats) {
|
||||
const gptConfig = GetGamePTConfig(stats.game, stats.playtype);
|
||||
|
||||
const aggRes: [{ _id: null; ranking: integer }] = await MONGODB_KILL["game-stats"].aggregate([
|
||||
{
|
||||
$match: {
|
||||
game: stats.game,
|
||||
playtype: stats.playtype,
|
||||
},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: null,
|
||||
ranking: {
|
||||
$sum: {
|
||||
$cond: {
|
||||
if: {
|
||||
$gt: [
|
||||
`$ratings.${gptConfig.defaultProfileRatingAlg}`,
|
||||
stats.ratings[gptConfig.defaultProfileRatingAlg],
|
||||
],
|
||||
},
|
||||
then: 1,
|
||||
else: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return aggRes[0].ranking + 1;
|
||||
const { ranking } = await GetUsersRankingAndOutOf(stats);
|
||||
return ranking;
|
||||
}
|
||||
|
||||
export function GetUGPTPlaycount(userID: integer, game: GameGroup, playtype: Playtype) {
|
||||
@@ -309,13 +279,12 @@ export async function GetUserPlayedGPTs(userID: integer) {
|
||||
}
|
||||
|
||||
export async function GetAllUserRivals(userID: integer) {
|
||||
const rivals = (
|
||||
await MONGODB_KILL["game-settings"].find({ userID }, { projection: { rivals: 1 } })
|
||||
)
|
||||
.map((e) => e.rivals)
|
||||
.flat();
|
||||
const rows = await DB.selectFrom("game_rival")
|
||||
.select("rival")
|
||||
.where("user_id", "=", userID)
|
||||
.execute();
|
||||
|
||||
return rivals;
|
||||
return [...new Set(rows.map((r) => r.rival))];
|
||||
}
|
||||
|
||||
const USERNAME_CHANGE_COOLDOWN = ONE_DAY * 180; // 6 months
|
||||
|
||||
Reference in New Issue
Block a user