sendou.ink/app/features/api-public/routes/user.$identifier.ts

146 lines
4.4 KiB
TypeScript

import { jsonArrayFrom } from "kysely/helpers/sqlite";
import type { LoaderFunctionArgs } from "react-router";
import { z } from "zod";
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";
import { requireBearerAuth } from "../api-public-utils.server";
import type { GetUserResponse } from "../schema";
const paramsSchema = z.object({
identifier: z.string(),
});
export const loader = async ({ params, request }: LoaderFunctionArgs) => {
requireBearerAuth(request);
const t = await i18next.getFixedT("en", ["weapons"]);
const { identifier } = parseParams({ params, schema: paramsSchema });
const user = notFoundIfFalsy(
await db
.selectFrom("User")
.leftJoin("PlusTier", "PlusTier.userId", "User.id")
.select(({ eb }) => [
"User.id",
"User.country",
"User.discordName",
"User.twitch",
"User.battlefy",
"User.bsky",
"User.customUrl",
"User.discordId",
"User.discordAvatar",
"User.pronouns",
"PlusTier.tier",
jsonArrayFrom(
eb
.selectFrom("UserWeapon")
.select(["UserWeapon.isFavorite", "UserWeapon.weaponSplId"])
.whereRef("UserWeapon.userId", "=", "User.id")
.orderBy("UserWeapon.order", "asc"),
).as("weapons"),
jsonArrayFrom(
eb
.selectFrom("BadgeOwner")
.innerJoin("Badge", "Badge.id", "BadgeOwner.badgeId")
.select(({ fn }) => [
"Badge.displayName",
"Badge.code",
fn.count<number>("BadgeOwner.badgeId").as("count"),
])
.groupBy(["BadgeOwner.badgeId", "BadgeOwner.userId"])
.whereRef("BadgeOwner.userId", "=", "User.id"),
).as("badges"),
jsonArrayFrom(
eb
.selectFrom("SplatoonPlayer")
.innerJoin(
"XRankPlacement",
"XRankPlacement.playerId",
"SplatoonPlayer.id",
)
.select(["XRankPlacement.power"])
.whereRef("SplatoonPlayer.userId", "=", "User.id"),
).as("xRankPlacements"),
jsonArrayFrom(
eb
.selectFrom("TeamMemberWithSecondary")
.innerJoin("Team", "Team.id", "TeamMemberWithSecondary.teamId")
.select(["Team.id", "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)
const parsedId =
identifier.length < 10 ? safeNumberParse(identifier) : null;
if (parsedId) {
return eb("User.id", "=", parsedId);
}
return eb("User.discordId", "=", identifier);
})
.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,
discordId: user.discordId,
avatarUrl: user.discordAvatar
? `https://cdn.discordapp.com/avatars/${user.discordId}/${user.discordAvatar}.png`
: null,
url: `https://sendou.ink/u/${user.customUrl ?? user.discordId}`,
country: user.country,
pronouns: user.pronouns,
plusServerTier: user.tier as GetUserResponse["plusServerTier"],
socials: {
twitch: user.twitch,
battlefy: user.battlefy,
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) => {
if (!cur.power) return acc;
return Math.max(acc, cur.power);
}, 0)
: null,
weaponPool: user.weapons.map((weapon) => ({
id: weapon.weaponSplId,
name: t(`weapons:MAIN_${weapon.weaponSplId}`),
isFiveStar: Boolean(weapon.isFavorite),
})),
badges: user.badges.map((badge) => ({
name: badge.displayName,
count: badge.count,
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) => ({
id: team.id,
role: team.role,
})),
};
return Response.json(result);
};