122 lines
4.1 KiB
TypeScript
122 lines
4.1 KiB
TypeScript
import { steam2ToSteam64 } from './steam';
|
|
|
|
export interface SteamSummary {
|
|
avatarUrl: string;
|
|
personaName: string | null;
|
|
}
|
|
|
|
// Steam's generic "no avatar set" question-mark image — used whenever a
|
|
// player has no avatar, or the lookup fails for any reason.
|
|
export const DEFAULT_AVATAR_URL =
|
|
'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/fe/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg';
|
|
|
|
/**
|
|
* Fetches avatar + persona name from Steam's GetPlayerSummaries endpoint.
|
|
* Always resolves to a usable avatarUrl (falls back to the default
|
|
* question-mark image on any failure — including a missing STEAM_API_KEY)
|
|
* rather than null, so callers don't need their own fallback logic.
|
|
*
|
|
* Cached for an hour via Next's extended fetch() — avatars rarely change,
|
|
* and this keeps calls well within Steam's rate limits without needing a
|
|
* separate DB cache table.
|
|
*/
|
|
export async function getSteamSummary(steamId64: string): Promise<SteamSummary> {
|
|
const apiKey = process.env.STEAM_API_KEY;
|
|
if (!apiKey) {
|
|
return { avatarUrl: DEFAULT_AVATAR_URL, personaName: null };
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(
|
|
`https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${apiKey}&steamids=${steamId64}`,
|
|
{ next: { revalidate: 3600 } },
|
|
);
|
|
if (!res.ok) {
|
|
return { avatarUrl: DEFAULT_AVATAR_URL, personaName: null };
|
|
}
|
|
|
|
const data = await res.json();
|
|
const player = data?.response?.players?.[0];
|
|
|
|
return {
|
|
avatarUrl: player?.avatarfull || DEFAULT_AVATAR_URL,
|
|
personaName: player?.personaname ?? null,
|
|
};
|
|
} catch {
|
|
return { avatarUrl: DEFAULT_AVATAR_URL, personaName: null };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Same as getSteamSummary, but for many players in one go — Steam's
|
|
* GetPlayerSummaries accepts up to 100 comma-separated steamids per call,
|
|
* so fetching a list of players (e.g. "who was online at this point in
|
|
* time") is one HTTP call per 100 players rather than one per player.
|
|
* Returns a map keyed by SteamID64; missing entries mean "use the default
|
|
* avatar" at the call site.
|
|
*/
|
|
export async function getSteamSummariesBatch(
|
|
steamId64List: string[],
|
|
): Promise<Map<string, SteamSummary>> {
|
|
const result = new Map<string, SteamSummary>();
|
|
const apiKey = process.env.STEAM_API_KEY;
|
|
if (!apiKey || steamId64List.length === 0) {
|
|
return result;
|
|
}
|
|
|
|
const chunks: string[][] = [];
|
|
for (let i = 0; i < steamId64List.length; i += 100) {
|
|
chunks.push(steamId64List.slice(i, i + 100));
|
|
}
|
|
|
|
await Promise.all(
|
|
chunks.map(async (chunk) => {
|
|
try {
|
|
const res = await fetch(
|
|
`https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${apiKey}&steamids=${chunk.join(',')}`,
|
|
{ next: { revalidate: 3600 } },
|
|
);
|
|
if (!res.ok) return;
|
|
|
|
const data = await res.json();
|
|
const players = data?.response?.players ?? [];
|
|
for (const player of players) {
|
|
result.set(player.steamid, {
|
|
avatarUrl: player.avatarfull || DEFAULT_AVATAR_URL,
|
|
personaName: player.personaname ?? null,
|
|
});
|
|
}
|
|
} catch {
|
|
// this chunk's players just fall back to the default avatar downstream
|
|
}
|
|
}),
|
|
);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Attaches avatarUrl to any list of objects that have a `steamid` (Steam2
|
|
* format) field — the shared enrichment step used everywhere a list of
|
|
* players gets rendered, so every list in the app shows avatars
|
|
* consistently without duplicating the batch-fetch/fallback logic per call
|
|
* site.
|
|
*/
|
|
export async function attachAvatars<T extends { steamid: string }>(
|
|
players: T[],
|
|
): Promise<(T & { avatarUrl: string })[]> {
|
|
const steam64ByPlayer = new Map<string, string>();
|
|
for (const p of players) {
|
|
const id64 = steam2ToSteam64(p.steamid);
|
|
if (id64) steam64ByPlayer.set(p.steamid, id64);
|
|
}
|
|
|
|
const avatars = await getSteamSummariesBatch([...steam64ByPlayer.values()]);
|
|
|
|
return players.map((p) => {
|
|
const id64 = steam64ByPlayer.get(p.steamid);
|
|
const avatarUrl = (id64 && avatars.get(id64)?.avatarUrl) || DEFAULT_AVATAR_URL;
|
|
return { ...p, avatarUrl };
|
|
});
|
|
}
|