Add fields to the user public API endpoint Closes #2425

This commit is contained in:
Kalle
2025-08-10 12:05:05 +03:00
parent af5e4d006b
commit 73c8c5121e
2 changed files with 90 additions and 0 deletions

View File

@@ -3,6 +3,8 @@ import { jsonArrayFrom } from "kysely/helpers/sqlite";
import { cors } from "remix-utils/cors";
import { z } from "zod/v4";
import { db } from "~/db/sql";
import * as Seasons from "~/features/mmr/core/Seasons";
import { userSkills as _userSkills } from "~/features/mmr/tiered.server";
import { i18next } from "~/modules/i18n/i18next.server";
import { safeNumberParse } from "~/utils/number";
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
@@ -68,6 +70,19 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
.select(["XRankPlacement.power"])
.whereRef("SplatoonPlayer.userId", "=", "User.id"),
).as("xRankPlacements"),
jsonArrayFrom(
eb
.selectFrom("TeamMemberWithSecondary")
.innerJoin("Team", "Team.id", "TeamMemberWithSecondary.teamId")
.select([
"Team.name",
"Team.customUrl",
"TeamMemberWithSecondary.role",
])
.whereRef("TeamMemberWithSecondary.userId", "=", "User.id")
.orderBy("TeamMemberWithSecondary.isMainTeam", "desc")
.orderBy("TeamMemberWithSecondary.createdAt", "asc"),
).as("teams"),
])
.where((eb) => {
// we don't want to parse discord id's as numbers (length = 18)
@@ -82,6 +97,11 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
.executeTakeFirst(),
);
const season = Seasons.currentOrPrevious(new Date())!.nth;
const { isAccurateTiers, userSkills } = _userSkills(season);
const skill = isAccurateTiers ? userSkills[user.id] : null;
const result: GetUserResponse = {
id: user.id,
name: user.discordName,
@@ -98,6 +118,12 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
bsky: user.bsky,
twitter: null, // deprecated field
},
currentRank: skill?.tier
? {
season,
tier: skill.tier,
}
: null,
peakXp:
user.xRankPlacements.length > 0
? user.xRankPlacements.reduce((acc, cur) => {
@@ -116,6 +142,11 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
gifUrl: `https://sendou.ink/static-assets/badges/${badge.code}.gif`,
imageUrl: `https://sendou.ink/static-assets/badges/${badge.code}.png`,
})),
teams: user.teams.map((team) => ({
name: team.name,
role: team.role,
teamPageUrl: `https://sendou.ink/t/${team.customUrl}`,
})),
};
return await cors(request, json(result));

View File

@@ -35,7 +35,11 @@ export interface GetUserResponse {
plusServerTier: 1 | 2 | 3 | null;
weaponPool: Array<ProfileWeapon>;
badges: Array<Badge>;
/** Teams user is member of. The main team is always first in the array. */
teams: Array<GlobalTeamMembership>;
peakXp: number | null;
/** Users current (or previous if it's off-season) ranked season (SendouQ & ranked tournaments) rank. Null if no rank for the season in question or the season does not have yet enough players on the leaderboard. */
currentRank: SeasonalRank | null;
}
/** GET /api/calendar/{year}/{week} */
@@ -329,6 +333,61 @@ type Weapon = {
type ProfileWeapon = Weapon & { isFiveStar: boolean };
interface GlobalTeamMembership {
/**
* Name of the global team.
*
* @example "Moonlight"
*/
name: string;
/**
* Role of the user in the team.
*/
role: TeamMemberRole | null;
/**
* URL for the global team page.
*
* @example "https://sendou.ink/t/moonlight"
*/
teamPageUrl: string;
}
type TeamMemberRole =
| "CAPTAIN"
| "CO_CAPTAIN"
| "FRONTLINE"
| "SLAYER"
| "SKIRMISHER"
| "SUPPORT"
| "MIDLINE"
| "BACKLINE"
| "FLEX"
| "SUB"
| "COACH"
| "CHEERLEADER";
interface SeasonalRank {
tier: {
name: RankTierName;
isPlus: boolean;
};
/**
* Which season this rank is for.
*
* @example 7
*/
season: number;
}
type RankTierName =
| "LEVIATHAN"
| "DIAMOND"
| "PLATINUM"
| "GOLD"
| "SILVER"
| "BRONZE"
| "IRON";
type Badge = {
/**
* @example "Monday Afterparty"