initial commit

This commit is contained in:
2024-12-26 18:16:46 +11:00
commit 83bc61c1a4
12 changed files with 2099 additions and 0 deletions

634
src/main.tsx Normal file
View File

@ -0,0 +1,634 @@
import Fastify from 'fastify'
import { Database } from './database.js';
import { 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 getChickenDinnerUrl(match: MatchResponse, player: PlayerResponse['data'][0]) {
const asset = match.included.find(i => i.type === 'asset')!;
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 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<string, MatchResponse> }) {
return (
<html>
<head>
<title>Matches</title>
<link rel="stylesheet" type="text/css" href="/style.css" />
<style>{`
li a { padding: 0 5px; }
#matches {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
}
`}</style>
</head>
<body>
<h1>Matches</h1>
{players.data.map(player => (
<>
<h2 id={player.attributes.name}>
{player.attributes.name}
</h2>
<div id="matches">
{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 date = dayjs(m.data.attributes.createdAt);
return <div>
<ul>
<li>Map: {map}</li>
<li>Mode: {PubgApi.modeName(m.data.attributes.gameMode)}</li>
<li>Date: {date.format('ddd DD/MM HH:mm')} ({date.fromNow()})</li>
<li>{playerName}'s rank: {rank}</li>
<li>Players: {count.humanCount} (bots: {count.botCount} - {`${Math.round(count.botCount / count.playerCount * 100)}%`})</li>
<li>Links:
<a href={`/match/${match.id}`}>Raw</a>
<a href={`https://pubglookup.com/players/steam/${player.attributes.name.toLowerCase()}/matches/${match.id}`}>PUBGLookup</a>
<a href={getChickenDinnerUrl(m, player)}>map replay</a>
</li>
</ul>
</div>
})}
</div>
</>
))}
</body>
</html>
);
}
fastify.get('/matches', async (req, res) => {
const players = await getCachedPlayers();
const matches: Record<string, MatchResponse> = {};
const matchIds = new Set<string>();
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(<MatchesHtml players={players} matches={matches} />);
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
}
html.contrast body {
color: #050505
}
html.contrast blockquote {
color: #11151a
}
html.contrast blockquote:before {
color: #262626
}
html.contrast a {
color: #03f
}
html.contrast a:visited {
color: #7d013e
}
html.contrast span.wr {
color: #800
}
html.contrast span.mfw {
color: #4d0000
}
@media screen and (prefers-color-scheme:light) {
html.inverted {
background-color: #000
}
html.inverted body {
color: #d9d9d9
}
html.inverted #contrast,
html.inverted #invmode {
color: #fff;
background-color: #000
}
html.inverted blockquote {
color: #d3c9be
}
html.inverted blockquote:before {
color: #b8b8b8
}
html.inverted a {
color: #00a2e7
}
html.inverted a:visited {
color: #ca1a70
}
html.inverted span.wr {
color: #d24637
}
html.inverted span.mfw {
color: #b00000
}
html.inverted.contrast {
background-color: #000
}
html.inverted.contrast body {
color: #fff
}
html.inverted.contrast #contrast,
html.inverted.contrast #invmode {
color: #fff;
background-color: #000
}
html.inverted.contrast blockquote {
color: #f8f6f5
}
html.inverted.contrast blockquote:before {
color: #e5e5e5
}
html.inverted.contrast a {
color: #44c7ff
}
html.inverted.contrast a:visited {
color: #e9579e
}
html.inverted.contrast span.wr {
color: #db695d
}
html.inverted.contrast span.mfw {
color: #ff0d0d
}
}
@media (prefers-color-scheme:dark) {
html:not(.inverted) {
background-color: #000
}
html:not(.inverted) body {
color: #d9d9d9
}
html:not(.inverted) #contrast,
html:not(.inverted) #invmode {
color: #fff;
background-color: #000
}
html:not(.inverted) blockquote {
color: #d3c9be
}
html:not(.inverted) blockquote:before {
color: #b8b8b8
}
html:not(.inverted) a {
color: #00a2e7
}
html:not(.inverted) a:visited {
color: #ca1a70
}
html:not(.inverted) span.wr {
color: #d24637
}
html:not(.inverted) span.mfw {
color: #b00000
}
html:not(.inverted).contrast {
background-color: #000
}
html:not(.inverted).contrast body {
color: #fff
}
html:not(.inverted).contrast #contrast,
html:not(.inverted).contrast #invmode {
color: #fff;
background-color: #000
}
html:not(.inverted).contrast blockquote {
color: #f8f6f5
}
html:not(.inverted).contrast blockquote:before {
color: #e5e5e5
}
html:not(.inverted).contrast a {
color: #44c7ff
}
html:not(.inverted).contrast a:visited {
color: #e9579e
}
html:not(.inverted).contrast span.wr {
color: #db695d
}
html:not(.inverted).contrast span.mfw {
color: #ff0d0d
}
html.inverted html {
background-color: #fefefe
}
}
a {
color: #07a
}
a:visited {
color: #941352
}
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
span.citneed {
vertical-align: top;
font-size: .7em;
padding-left: .3em
}
small {
font-size: .4em
}
p.st {
margin-top: -1em
}
div.fancyPositioning div.picture-left {
float: left;
width: 40%;
overflow: hidden;
margin-right: 1em
}
div.fancyPositioning div.picture-left img {
width: 100%
}
div.fancyPositioning div.picture-left figure {
margin: 10px
}
div.fancyPositioning div.picture-left figure figcaption {
font-size: .7em
}
div.fancyPositioning div.tleft {
float: left;
width: 55%
}
div.fancyPositioning div.tleft p:first-child {
margin-top: 0
}
div.fancyPositioning:after {
display: block;
content: "";
clear: both
}
ul li img {
height: 1em
}
blockquote {
color: #456;
margin-left: 0;
margin-top: 2em;
margin-bottom: 2em
}
blockquote span {
float: left;
margin-left: 1rem;
padding-top: 1rem
}
blockquote author {
display: block;
clear: both;
font-size: .6em;
margin-left: 2.4rem;
font-style: oblique
}
blockquote author:before {
content: "- ";
margin-right: 1em
}
blockquote:before {
font-family: Times New Roman, Times, Arial;
color: #666;
content: open-quote;
font-size: 2.2em;
font-weight: 600;
float: left;
margin-top: 0;
margin-right: .2rem;
width: 1.2rem
}
blockquote:after {
content: "";
display: block;
clear: both
}
@media screen and (max-width:500px) {
body {
text-align: left
}
div.fancyPositioning div.picture-left,
div.fancyPositioning div.tleft {
float: none;
width: inherit
}
blockquote span {
width: 80%
}
blockquote author {
padding-top: 1em;
width: 80%;
margin-left: 15%
}
blockquote author:before {
content: "";
margin-right: inherit
}
}
span.visited {
color: #941352
}
span.visited-maroon {
color: #85144b
}
span.wr {
color: #c0392b;
font-weight: 600
}
button.cont-inv,
span.wr {
text-decoration: underline
}
button.cont-inv {
cursor: pointer;
border-radius: 2px;
position: fixed;
right: 10px;
font-size: .8em;
border: 0;
padding: 2px 5px
}
#contrast {
color: #000;
top: 10px
}
#contrast,
#invmode {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none
}
#invmode {
color: #fff;
background-color: #000;
position: fixed;
top: 34px;
text-decoration: underline
}
@media screen and (max-width:1080px) {
#contrast,
#invmode {
position: absolute
}
}
span.sb {
color: #00e
}
span.sb,
span.sv {
cursor: not-allowed
}
span.sv {
color: #551a8b
}
span.foufoufou {
color: #444;
font-weight: 700
}
span.foufoufou:before {
content: "";
display: inline-block;
width: 1em;
height: 1em;
margin-left: .2em;
margin-right: .2em;
background-color: #444
}
span.foufivfoufivfoufiv {
color: #454545;
font-weight: 700
}
span.foufivfoufivfoufiv:before {
content: "";
display: inline-block;
width: 1em;
height: 1em;
margin-left: .2em;
margin-right: .2em;
background-color: #454545
}
span.mfw {
color: #730000
}
a.kopimi,
a.kopimi img.kopimi {
display: block;
margin-left: auto;
margin-right: auto
}
a.kopimi img.kopimi {
height: 2em
}
p.fakepre {
font-family: monospace;
font-size: .9em
}
`;
});
// Run the server!
try {
await fastify.listen({ port: 3000, host: '0.0.0.0' })
} catch (err) {
fastify.log.error(err)
process.exit(1)
}