From a4127960b70f4d23cc29705931e9ccdb6f9475bf Mon Sep 17 00:00:00 2001 From: Aotumuri Date: Mon, 15 Sep 2025 07:15:50 +0900 Subject: [PATCH] Add function to fetch player information (#2010) ## Description: This pull request introduces the fetchPlayerById() function together with its associated schema components. It represents one part of a series of split pull requests related to the PlayerInfoModal (Player Profile). Subsequent pull requests will address UI implementation and additional features. (origin pr:https://github.com/openfrontio/OpenFrontIO/pull/1758) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: aotumuri --------- Co-authored-by: evanpelle --- src/client/jwt.ts | 41 ++++++++++++++++++++++++++++++++++++++++ src/core/ApiSchemas.ts | 40 ++++++++++++++++++++++++++++++++++++++- src/core/StatsSchemas.ts | 2 +- 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/client/jwt.ts b/src/client/jwt.ts index 6ada4c9fb..c3222cb5d 100644 --- a/src/client/jwt.ts +++ b/src/client/jwt.ts @@ -1,6 +1,8 @@ import { decodeJwt } from "jose"; import { z } from "zod"; import { + PlayerProfile, + PlayerProfileSchema, RefreshResponseSchema, TokenPayload, TokenPayloadSchema, @@ -268,3 +270,42 @@ export async function getUserMe(): Promise { return false; } } + +export async function fetchPlayerById( + playerId: string, +): Promise { + try { + const base = getApiBase(); + const token = getToken(); + if (!token) return false; + const url = `${base}/player/${encodeURIComponent(playerId)}`; + + const res = await fetch(url, { + headers: { + Accept: "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + if (res.status !== 200) { + console.warn( + "fetchPlayerById: unexpected status", + res.status, + res.statusText, + ); + return false; + } + + const json = await res.json(); + const parsed = PlayerProfileSchema.safeParse(json); + if (!parsed.success) { + console.warn("fetchPlayerById: Zod validation failed", parsed.error); + return false; + } + + return parsed.data; + } catch (err) { + console.warn("fetchPlayerById: request failed", err); + return false; + } +} diff --git a/src/core/ApiSchemas.ts b/src/core/ApiSchemas.ts index 262e06c9a..17ee8aa1a 100644 --- a/src/core/ApiSchemas.ts +++ b/src/core/ApiSchemas.ts @@ -1,5 +1,7 @@ import { z } from "zod"; import { base64urlToUuid } from "./Base64"; +import { BigIntStringSchema, PlayerStatsSchema } from "./StatsSchemas"; +import { Difficulty, GameMapType, GameMode, GameType } from "./game/Game"; export const RefreshResponseSchema = z.object({ token: z.string(), @@ -37,8 +39,8 @@ export const DiscordUserSchema = z.object({ username: z.string(), global_name: z.string().nullable(), discriminator: z.string(), - locale: z.string().optional(), }); +export type DiscordUser = z.infer; export const UserMeResponseSchema = z.object({ user: z.object({ @@ -52,3 +54,39 @@ export const UserMeResponseSchema = z.object({ }), }); export type UserMeResponse = z.infer; + +export const PlayerStatsLeafSchema = z.object({ + wins: BigIntStringSchema, + losses: BigIntStringSchema, + total: BigIntStringSchema, + stats: PlayerStatsSchema, +}); +export type PlayerStatsLeaf = z.infer; + +export const PlayerStatsTreeSchema = z.partialRecord( + z.enum(GameType), + z.partialRecord( + z.enum(GameMode), + z.partialRecord(z.enum(Difficulty), PlayerStatsLeafSchema), + ), +); +export type PlayerStatsTree = z.infer; + +export const PlayerGameSchema = z.object({ + gameId: z.string(), + start: z.iso.datetime(), + mode: z.enum(GameMode), + type: z.enum(GameType), + map: z.enum(GameMapType), + difficulty: z.enum(Difficulty), + clientId: z.string().optional(), +}); +export type PlayerGame = z.infer; + +export const PlayerProfileSchema = z.object({ + createdAt: z.iso.datetime(), + user: DiscordUserSchema.optional(), + games: PlayerGameSchema.array(), + stats: PlayerStatsTreeSchema, +}); +export type PlayerProfile = z.infer; diff --git a/src/core/StatsSchemas.ts b/src/core/StatsSchemas.ts index 41cb3a47e..1a4a949ec 100644 --- a/src/core/StatsSchemas.ts +++ b/src/core/StatsSchemas.ts @@ -88,7 +88,7 @@ export const OTHER_INDEX_CAPTURE = 2; // Structures captured export const OTHER_INDEX_LOST = 3; // Structures/warships destroyed/captured by others export const OTHER_INDEX_UPGRADE = 4; // Structures upgraded -const BigIntStringSchema = z.preprocess((val) => { +export const BigIntStringSchema = z.preprocess((val) => { if (typeof val === "string" && /^-?\d+$/.test(val)) return BigInt(val); if (typeof val === "bigint") return val; return val;