import Fastify from 'fastify' import { Database } from './database.js'; import { Asset, MatchResponse, PlayerResponse, PubgApi } from './pubg-api.js'; import { Mutex } from 'async-mutex'; import { render, renderToStringAsync } from 'preact-render-to-string'; import { h, Fragment } from 'preact'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc.js' import relativeTime from 'dayjs/plugin/relativeTime.js' dayjs.extend(utc); dayjs.extend(relativeTime); const fastify = Fastify({ logger: true }); const db = await Database(); const api = new PubgApi(); const playerMutex = new Mutex(); async function getCachedPlayers() { return await playerMutex.runExclusive(async () => { const playerNames = ['Pigophone', 'The-High-Ground', 'Tokar-TK']; const cached = await db.getCachedPlayer(); if (cached) return cached; const players = await api.getPlayers(playerNames); await db.savePlayer(players); return players; }); } async function getMatch(id: string) { const cached = await db.getMatch(id); if (cached) return cached; const match = await api.getMatch(id); await db.saveMatch(match); return match; } function getBotCount(match: MatchResponse) { // bots have account id starting with 'ai.' let playerCount = 0; let botCount = 0; for (const player of match.included) { if (player.type === 'participant') { playerCount++; if (player.attributes.stats.playerId.startsWith('ai.')) { botCount++; } } } return { playerCount, botCount, humanCount: playerCount - botCount }; } function getTelemetry(match: MatchResponse) { const asset = match.included.find(i => i.type === 'asset')!; return asset!; } function getChickenDinnerUrl(asset: Asset, player: PlayerResponse['data'][0]) { const url = asset.attributes.URL.substring('https://telemetry-cdn.pubg.com/bluehole-pubg/'.length).replace('-telemetry.json', ''); return `https://chickendinner.gg/${url}?follow=${player.attributes.name}`; } function getChickenDinnerUrlv2(matchId: string, player: PlayerResponse['data'][0]) { return `https://chickendinner.gg/${matchId}/${player.attributes.name}`; } function getRank(match: MatchResponse, playerId: string) { for (const player of match.included) { if (player.type === 'participant' && player.attributes.stats.playerId === playerId) { return player.attributes.stats.winPlace; } } return 'N/A'; } function MatchesHtml({ players, matches }: { players: PlayerResponse, matches: Record }) { return ( Matches

Matches

{players.data.map(player => ( <>

{player.attributes.name}

{player.relationships.matches.data.map(match => { var m = matches[match.id]; const playerName = player.attributes.name; var map = PubgApi.mapName(m.data.attributes.mapName); var rank = getRank(m, player.id); var count = getBotCount(m); const telemetry = getTelemetry(m); const date = dayjs(telemetry.attributes.createdAt); return
  • Map: {map}
  • Mode: {PubgApi.modeName(m.data.attributes.gameMode)}
  • Date: {date.format('ddd DD/MM HH:mm')} ({date.fromNow()})
  • {playerName}'s rank: {rank}
  • Players: {count.humanCount} (bots: {count.botCount} - {`${Math.round(count.botCount / count.playerCount * 100)}%`})
  • Links: Raw PUBGLookup map replay
})}
))} ); } fastify.get('/matches', async (req, res) => { const players = await getCachedPlayers(); const matches: Record = {}; const matchIds = new Set(); for (const player of players.data) { for (const match of player.relationships.matches.data) { matchIds.add(match.id); } } await Promise.all([...matchIds].map(async id => { matches[id] = await getMatch(id); })); const html = render(); res.header('Content-Type', 'text/html'); return html; }); fastify.get<{ Params: { id: string } }>('/match/:id', async (req, res) => { const match = await getMatch(req.params.id); return match; }); fastify.get('/style.css', async (req, res) => { res.header('Content-Type', 'text/css'); return ` * { box-sizing: border-box; } body { font-family: Open Sans, Arial; color: #454545; font-size: 16px; margin: 2em auto; max-width: 1600px; padding: 1em; line-height: 1.4; -webkit-hyphens: auto; -ms-hyphens: auto; hyphens: auto } a { color: #07a } a:visited { color: #941352 } `; }); // Run the server! try { await fastify.listen({ port: 3000, host: '0.0.0.0' }) } catch (err) { fastify.log.error(err) process.exit(1) }