Files
www/src/server/api/routers/history.ts
2025-04-04 02:12:45 +02:00

175 lines
4.5 KiB
TypeScript

import { createTRPCRouter, publicProcedure } from '@/server/api/trpc'
import { db } from '@/server/db'
import { metadata, player_games, raw_history } from '@/server/db/schema'
import { desc, eq } from 'drizzle-orm'
import ky from 'ky'
import { chunk } from 'remeda'
import { z } from 'zod'
export const history_router = createTRPCRouter({
user_games: publicProcedure
.input(
z.object({
user_id: z.string(),
})
)
.query(async ({ ctx, input }) => {
return await ctx.db
.select()
.from(player_games)
.where(eq(player_games.playerId, input.user_id))
.orderBy(desc(player_games.gameNum))
}),
sync: publicProcedure.mutation(async () => {
return syncHistory()
}),
})
export async function syncHistory() {
const cursor = await db
.select()
.from(metadata)
.where(eq(metadata.key, 'history_cursor'))
.limit(1)
.then((res) => res[0])
const data = await ky
.get('https://api.neatqueue.com/api/history/1226193436521267223', {
searchParams: {
start_game_number: cursor?.value ?? 1,
},
timeout: 60000,
})
.json<any>()
const matches = await fetch(
'https://api.neatqueue.com/api/matches/1226193436521267223'
).then((res) => res.json())
const firstGame = Object.keys(matches).sort(
(a, b) => Number.parseInt(a) - Number.parseInt(b)
)[0]
if (!firstGame) {
throw new Error('No first game found')
}
await db
.insert(metadata)
.values({
key: 'history_cursor',
value: firstGame,
})
.onConflictDoUpdate({
target: metadata.key,
set: {
key: 'history_cursor',
value: firstGame,
},
})
const chunkedData = chunk(data.data, 100)
for (const chunk of chunkedData) {
await insertGameHistory(chunk).catch((e) => {
console.error(e)
})
}
return data
}
function processGameEntry(gameId: number, game_num: number, entry: any) {
const parsedEntry = typeof entry === 'string' ? JSON.parse(entry) : entry
if (parsedEntry.game === '1v1-attrition') {
return []
}
if (!parsedEntry.teams?.[0]?.[0] || !parsedEntry.teams?.[1]?.[0]) {
console.log('skipping game', parsedEntry)
return []
}
if (parsedEntry.winner === -2) {
console.log('skipping ongoing game', parsedEntry)
return []
}
const player0 = parsedEntry.teams[0][0]
const player1 = parsedEntry.teams[1][0]
let p0result = null
let p1result = null
if (parsedEntry.winner === 2) {
p0result = 'tie'
p1result = 'tie'
} else if (parsedEntry.winner === 0) {
p0result = 'win'
p1result = 'loss'
} else if (parsedEntry.winner === 1) {
p0result = 'loss'
p1result = 'win'
} else {
p0result = 'unknown'
p1result = 'unknown'
}
return [
{
gameId,
gameNum: game_num,
gameTime: new Date(parsedEntry.time),
gameType: parsedEntry.game,
mmrChange: Number.parseFloat(player0.mmr_change),
opponentId: player1.id,
opponentMmr: Number.parseFloat(player1.mmr),
opponentName: player1.name,
playerId: player0.id,
playerMmr: Number.parseFloat(player0.mmr),
playerName: player0.name,
result: p0result,
won: parsedEntry.winner === 0,
},
{
gameId,
gameNum: game_num,
gameTime: new Date(parsedEntry.time),
gameType: parsedEntry.game,
mmrChange: Number.parseFloat(player1.mmr_change),
opponentId: player0.id,
opponentMmr: Number.parseFloat(player0.mmr),
opponentName: player0.name,
playerId: player1.id,
playerMmr: Number.parseFloat(player1.mmr),
playerName: player1.name,
result: p1result,
won: parsedEntry.winner === 1,
},
]
}
export async function insertGameHistory(entries: any[]) {
const rawResults = await Promise.all(
entries.map(async (entry) => {
return db
.insert(raw_history)
.values({ entry, game_num: entry.game_num })
.returning()
.onConflictDoUpdate({
target: raw_history.game_num,
set: {
entry,
},
})
.then((res) => res[0])
})
).then((res) => res.filter(Boolean))
const playerGameRows = rawResults.flatMap(({ entry, id, game_num }: any) => {
return processGameEntry(id, game_num, entry)
})
await Promise.all(
playerGameRows.map(async (row) => {
return db
.insert(player_games)
.values(row)
.onConflictDoUpdate({
target: [player_games.playerId, player_games.gameNum],
set: row,
})
.then((res) => res[0])
})
)
}