Merge pull request #23 from Legonzaur/main

v2
This commit is contained in:
Legonzaur
2023-04-25 13:08:49 +02:00
committed by GitHub
16 changed files with 946 additions and 133 deletions
+1 -4
View File
@@ -2,7 +2,4 @@ POSTGRES_HOST="localhost"
POSTGRES_DATABASE="postgres"
POSTGRES_USER="postgres"
POSTGRES_PASSWORD="postgres"
REDIS_USER="default"
REDIS_PASSWORD=""
REDIS_URL="redis://awesome.redis.server:6379"
SERVERAUTH_TEST="1:f9f8fc8c-9185-47a8-8a99-9e3acf6ca007"
SERVERAUTH_TOKEN="1:f9f8fc8c-9185-47a8-8a99-9e3acf6ca007"
+63 -29
View File
File diff suppressed because one or more lines are too long
+453
View File
@@ -0,0 +1,453 @@
openapi: 3.0.2
info:
title: ToneAPI
version: '2.0'
contact:
name: Legonzaur
url: 'https://github.com/Legonzaur'
license:
name: The Unlicense
url: 'https://unlicense.org/'
description: |-
Stats tracking API for Titanfall 2 Northstar
All query params can be negated using `!`. Example : (`https://tone.sleepycat.date/v2/client/gamemodes?gamemode=!sns`)
tags:
- name: client
description: Routes accessibles by everyone
- name: server
description: Routes accessibles only to server owners
servers:
- url: 'https://tone.sleepycat.date/v2'
paths:
/client/hosts:
get:
summary: Your GET endpoint
tags:
- client
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
'[number] host_id':
type: string
example: Fvnkhead
examples:
Example 1:
value:
'1': Fvnkhead
'2': Legonzaur
operationId: get-client-hosts
description: An object with host ID as keys and host name as values
/client/servers:
get:
summary: List of servers
tags:
- client
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
'[string] server_name':
type: object
properties:
deaths:
type: integer
kills:
type: integer
max_distance:
type: integer
total_distance:
type: integer
host:
type: integer
examples:
Example 1:
value:
fvnkhead's 3v3:
deaths: 79810
kills: 76218
max_distance: 3733
total_distance: 42200935
host: 1
fvnkhead's 7v7:
deaths: 148609
kills: 163272
max_distance: 6578
total_distance: 113915706
host: 1
operationId: get-server-list
description: A JSON object with server names as keys and server data as values
parameters:
- $ref: '#/components/parameters/server'
- $ref: '#/components/parameters/player'
- $ref: '#/components/parameters/weapon'
- $ref: '#/components/parameters/map'
- $ref: '#/components/parameters/gamemode'
- $ref: '#/components/parameters/host'
parameters: []
/client/weapons:
get:
summary: Statistics for all weapons
tags:
- client
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
'[string] weapon_id':
type: object
properties:
deaths:
type: integer
description: Number of deaths caused by this weapon
kills:
type: integer
max_distance:
type: integer
total_distance:
type: integer
deaths_while_equipped:
type: integer
description: Number of deaths while this weapon is equipped
examples:
Example 1:
value:
autopistol:
deaths: 27
kills: 28
max_distance: 2173
total_distance: 12792
car:
deaths: 1394
kills: 1534
max_distance: 3217
total_distance: 905505
operationId: get-client-weapons
parameters:
- $ref: '#/components/parameters/server'
- $ref: '#/components/parameters/player'
- $ref: '#/components/parameters/weapon'
- $ref: '#/components/parameters/map'
- $ref: '#/components/parameters/gamemode'
- $ref: '#/components/parameters/host'
requestBody:
content: {}
description: A JSON Object with weapon IDS as keys and weapon data as value
description: A JSON Object with weapon IDs as keys and weapon data as values
parameters: []
/client/players:
get:
summary: Statistics for all players
tags:
- client
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
'[number] player_id':
type: object
properties:
deaths:
type: integer
kills:
type: integer
max_distance:
type: integer
total_distance:
type: integer
username:
type: string
examples:
Example 1:
value:
'2250125460':
username: Legonzaur
deaths: 0
kills: 0
max_kill_distance: 0
avg_kill_distance: 0
operationId: get-client-players
parameters:
- $ref: '#/components/parameters/weapon'
- $ref: '#/components/parameters/server'
- $ref: '#/components/parameters/player'
- $ref: '#/components/parameters/gamemode'
- $ref: '#/components/parameters/map'
- $ref: '#/components/parameters/host'
description: A JSON Object with player IDs as keys and player data as values
/client/maps:
get:
summary: Your GET endpoint
tags:
- client
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
'[string] map_id':
$ref: '#/components/schemas/KillData'
examples:
Example 1:
value:
lf_meadow:
deaths: 13628
kills: 11791
max_distance: 3970
total_distance: 7139763
lf_township:
deaths: 14861
kills: 14232
max_distance: 3642
total_distance: 9022513
operationId: get-client-maps
parameters:
- $ref: '#/components/parameters/host'
- $ref: '#/components/parameters/gamemode'
- $ref: '#/components/parameters/map'
- $ref: '#/components/parameters/weapon'
- $ref: '#/components/parameters/player'
- $ref: '#/components/parameters/server'
description: A JSON Object with map IDs as keys and map data as values
/client/gamemodes:
get:
summary: Your GET endpoint
tags:
- client
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
'[string] gamemode_id':
$ref: '#/components/schemas/KillData'
examples:
Example 1:
value:
ps:
deaths: 273232
kills: 286679
max_distance: 8521
total_distance: 184069789
aitdm:
deaths: 11388
kills: 12814
max_distance: 5689
total_distance: 9184028
operationId: get-client-gamemodes
description: A JSON Object with gamemodes IDs as keys and gamemodes data as values
parameters:
- $ref: '#/components/parameters/host'
- $ref: '#/components/parameters/gamemode'
- $ref: '#/components/parameters/map'
- $ref: '#/components/parameters/weapon'
- $ref: '#/components/parameters/player'
- $ref: '#/components/parameters/server'
/servers:
parameters: []
post:
tags:
- server
summary: ''
operationId: post-servers-serverId
responses:
'200':
description: OK
'403':
description: Forbidden
description: Used to test auth
security:
- Auth_Token: []
/servers/kills:
post:
tags:
- server
summary: Post kills
operationId: post-kills
responses:
'201':
description: Created
'400':
description: Bad Request
'401':
description: Unauthorized
parameters: []
security:
- Auth_Token: []
description: Allows a Northstar server to record kills on the database
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Kill'
description: ''
parameters: []
components:
schemas:
KillData:
title: Killdata
type: object
properties:
deaths:
type: integer
kills:
type: integer
max_kill_distance:
type: integer
avg_kill_distance:
type: number
Kill:
title: Kill
type: object
properties:
id:
type: integer
readOnly: true
server:
type: string
readOnly: true
tone_version:
type: string
match_id:
type: string
game_mode:
type: string
map:
type: string
game_time:
type: string
format: time
player_count:
type: integer
attacker_name:
type: string
attacker_id:
type: integer
attacker_current_weapon:
type: string
attacker_current_weapon_mods:
type: integer
attacker_weapon_1:
type: string
attacker_weapon_1_mods:
type: integer
attacker_weapon_2:
type: string
attacker_weapon_2_mods:
type: integer
attacker_weapon_3:
type: string
attacker_weapon_3_mods:
type: integer
attacker_offhand_weapon_1:
type: integer
attacker_offhand_weapon_2:
type: integer
victim_name:
type: string
victim_id:
type: integer
victim_current_weapon:
type: string
victim_current_weapon_mods:
type: integer
victim_weapon_1:
type: string
victim_weapon_1_mods:
type: integer
victim_weapon_2:
type: string
victim_weapon_2_mods:
type: integer
victim_weapon_3:
type: string
victim_weapon_3_mods:
type: integer
victim_offhand_weapon_1:
type: integer
victim_offhand_weapon_2:
type: integer
cause_of_death:
type: string
distance:
type: number
required:
- cause_of_death
securitySchemes:
Auth_Token:
type: http
scheme: bearer
parameters:
server:
name: server
in: query
required: false
schema:
type: string
example: fvnkhead's 3v3
description: Fetch data for specific server
player:
name: player
in: query
required: false
schema:
type: integer
example: 1005930844007
description: Fetch data for specific player
weapon:
name: weapon
in: query
required: false
schema:
type: string
example: epg
description: Fetch data for specific weapon
map:
name: map
in: query
required: false
schema:
type: string
example: lf_stacks
description: Fetch data for specific map
gamemode:
name: gamemode
in: query
required: false
schema:
type: string
example: aitdm
description: Fetch data for specific gamemode
host:
name: host
in: query
required: false
schema:
type: integer
example: 1
description: Fetch data for specific host
responses: {}
examples: {}
+1 -1
View File
@@ -33,7 +33,7 @@
"build": "npx tsc",
"startServer": "node out/serverMain.js",
"startClient": "node out/clientMain.js",
"documentation": "redocly build-docs .\\docs\\v1.yml --output=docs\\index.html",
"documentation": "redocly build-docs .\\docs\\v2.yml --output=docs\\index.html",
"test": "jest --runInBand"
}
}
+77 -25
View File
@@ -10,34 +10,48 @@ router.get('/*', (req, res, next) => {
next()
})
function filters(e: any, query: any) {
function computeQueryParam(
queryParam: string,
comparison: string | number
): boolean {
return queryParam.startsWith('!')
? queryParam.substring(1) != comparison
: queryParam == comparison
}
function filters(e: typeof allData[0], query: any) {
return (
(query.player ? query.player == e.attacker_id : true) &&
(query.server ? query.server == e.servername : true) &&
(query.host ? Number(query.host) == e.host : true) &&
(query.map ? query.map == e.map : true) &&
(query.weapon ? query.weapon == e.cause_of_death : true) &&
(query.gamemode ? query.gamemode == e.game_mode : true)
(query.player ? computeQueryParam(query.player, e.attacker_id) : true) &&
(query.server ? computeQueryParam(query.server, e.servername) : true) &&
(query.host ? computeQueryParam(query.host, e.host) : true) &&
(query.map ? computeQueryParam(query.map, e.map) : true) &&
(query.weapon ? computeQueryParam(query.weapon, e.cause_of_death) : true) &&
(query.gamemode ? computeQueryParam(query.gamemode, e.game_mode) : true)
)
}
router.get('/hosts',
async (req, res) => {
const result = (await getHostList())
router.get('/hosts', async (req, res) => {
const result = await getHostList()
const data: { [key: number]: string } = {}
result.forEach(e => data[Number(e.id)] = e.name)
result.forEach((e) => (data[Number(e.id)] = e.name))
res.status(200).send(data)
})
})
router.get(
'/:dataType',
param('dataType')
.custom(
(e) => e == 'weapons' || e == 'players' || e == 'maps' || e == 'servers'
(e) =>
e == 'weapons' ||
e == 'players' ||
e == 'maps' ||
e == 'servers' ||
e == 'gamemodes'
)
.withMessage('Only weapons, players, maps or servers are valid paths'),
query(['player', 'host']).optional().toInt().isInt(),
query(['server', 'map', 'weapon', 'gamemode']).optional().isString(),
query(['server', 'map', 'weapon', 'gamemode', 'player', 'host'])
.optional()
.isString(),
validateErrors,
(req, res) => {
const data: {
@@ -48,9 +62,15 @@ router.get(
total_distance: number
username?: string
host?: number
deaths_while_equipped?: number
}
} = {}
let index: 'cause_of_death' | 'attacker_id' | 'map' | 'servername'
let index:
| 'cause_of_death'
| 'attacker_id'
| 'map'
| 'servername'
| 'game_mode'
switch (req.params.dataType) {
case 'weapons':
index = 'cause_of_death'
@@ -64,29 +84,61 @@ router.get(
case 'servers':
index = 'servername'
break
case 'gamemodes':
index = 'game_mode'
break
default:
return res.status(400).send()
}
const timeStart = new Date()
allData
.filter((e) => filters(e, req.query))
.forEach((e) => {
if (!data[e[index]])
data[e[index]] = {
if (
!e.cause_of_death ||
!e.attacker_id ||
!e.map ||
!e.servername ||
!e.game_mode
)
return
const requestIndex = e[index]
if (!requestIndex) return
if (!data[requestIndex])
data[requestIndex] = {
deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0
}
if (index === 'attacker_id') data[e[index]].username = e.attacker_name
if (index === 'servername') data[e[index]].host = e.host
data[e[index]].deaths += Number(e.deaths)
data[e[index]].kills += Number(e.kills)
data[e[index]].total_distance += Number(e.total_distance)
data[e[index]].max_distance = Math.max(
data[e[index]].max_distance,
if (index === 'attacker_id')
data[requestIndex].username = e.attacker_name
if (index === 'servername') data[requestIndex].host = e.host
if (
index === 'cause_of_death' ||
(req.query.weapon &&
!req.query.weapon.toString().startsWith('!') &&
index === 'attacker_id')
)
data[requestIndex].deaths_while_equipped =
Number(e.deaths_with_weapon) +
(data[requestIndex].deaths_while_equipped || 0)
data[requestIndex].deaths += Number(e.deaths)
data[requestIndex].kills += Number(e.kills)
data[requestIndex].total_distance += Number(e.total_distance)
data[requestIndex].max_distance = Math.max(
data[requestIndex].max_distance,
Number(e.max_distance)
)
})
console.log(
new Date().toLocaleString() + ',' +
(req.headers['x-forwarded-for']?.toString() ||
req.socket.remoteAddress?.toString() ||
'') +
',' +
Math.abs(new Date().getTime() - timeStart.getTime()) / 1000 + ',' + req.originalUrl
)
res.status(200).send(data)
}
)
+2
View File
@@ -5,6 +5,7 @@ import cors from 'cors'
import client from './client/client'
import { dbReady } from './db/db'
import processAll from './process/process'
import listenKills from './process/onKill'
const app = express()
const port = 3000
@@ -22,6 +23,7 @@ export default
new Promise(async (resolve, reject) => {
await dbReady()
await processAll()
await listenKills()
const listenServer = app.listen(port, '0.0.0.0', () => {
console.log(`Tone client api listening on port ${port}`)
resolve(listenServer)
@@ -0,0 +1,31 @@
import { Kysely, sql } from 'kysely'
import { Client } from 'pg'
const pgClient = new Client({
host: process.env.POSTGRES_HOST,
database: process.env.POSTGRES_DATABASE,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD
})
export async function up(db: Kysely<any>): Promise<void> {
await pgClient.connect()
await pgClient.query(`ALTER TABLE kill ADD COLUMN IF NOT EXISTS attacker_titan character varying NULL;`)
await pgClient.query(`UPDATE kill SET attacker_titan = 'scorch' WHERE (cause_of_death = 'mp_titancore_flame_wave_secondary' OR cause_of_death = 'mp_titancore_flame_wave' OR cause_of_death = 'mp_titancore_flame_wall' OR cause_of_death = 'mp_titanweapon_meteor_thermite' OR cause_of_death = 'mp_titanweapon_heat_shield' OR cause_of_death = 'titan_punch_scorch' OR cause_of_death = 'mp_titanweapon_meteor')`)
await pgClient.query(`UPDATE kill SET attacker_titan = 'ronin' WHERE (cause_of_death = 'mp_titanweapon_arc_wave' OR cause_of_death = 'mp_titancore_shift_core' OR cause_of_death = 'titan_sword' OR cause_of_death = 'mp_titanweapon_leadwall')`)
await pgClient.query(`UPDATE kill SET attacker_titan = 'ion' WHERE (cause_of_death = 'titan_punch_ion' OR cause_of_death = 'mp_titanweapon_vortex_shield_ion' OR cause_of_death = 'mp_titancore_laser_cannon' OR cause_of_death = 'mp_titanability_laser_trip' OR cause_of_death = 'mp_titanweapon_laser_lite')`)
await pgClient.query(`UPDATE kill SET attacker_titan = 'tone' WHERE (cause_of_death = 'mp_titancore_salvo_core' OR cause_of_death = 'titan_punch_tone' OR cause_of_death = 'mp_titanweapon_salvo_rockets' OR cause_of_death = 'mp_titanweapon_sticky_40mm')`)
await pgClient.query(`UPDATE kill SET attacker_titan = 'northstar' WHERE (cause_of_death = 'mp_titanability_slow_trap' OR cause_of_death = 'mp_titanweapon_flightcore_rockets' OR cause_of_death = 'titan_punch_northstar' OR cause_of_death = 'mp_titanweapon_sniper')`)
await pgClient.query(`UPDATE kill SET attacker_titan = 'legion' WHERE (cause_of_death = 'titan_punch_legion' OR cause_of_death = 'mp_titanweapon_predator_cannon')`)
await pgClient.query(`UPDATE kill SET attacker_titan = 'vanguard' WHERE (cause_of_death = 'mp_titanweapon_dumbfire_rockets' OR cause_of_death = 'titan_punch_vanguard' OR cause_of_death = 'mp_titanweapon_tracker_rockets' OR cause_of_death = 'mp_titanweapon_particle_accelerator' OR cause_of_death = 'mp_titanweapon_xo16_vanguard')`)
await pgClient.query(`ALTER TABLE kill ADD COLUMN IF NOT EXISTS victim_titan character varying NULL;`)
//Update the hosts column once the hoster table is manually updated
await pgClient.end()
}
export async function down(db: Kysely<any>): Promise<void> {
await pgClient.connect()
pgClient.query('ALTER TABLE kill DROP COLUMN IF EXISTS attacker_titan')
pgClient.query('ALTER TABLE kill DROP COLUMN IF EXISTS victim_titan')
await pgClient.end()
}
@@ -0,0 +1,84 @@
import { Kysely, sql } from 'kysely'
import { Client } from 'pg'
const pgClient = new Client({
host: process.env.POSTGRES_HOST,
database: process.env.POSTGRES_DATABASE,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD
})
const weapons = [
'car',
'alternator_smg',
'hemlok_smg',
'r97',
'hemlok',
'vinson',
'g2',
'rspn101',
'rspn101_og',
'esaw',
'lstar',
'lmg',
'shotgun',
'mastiff',
'dmr',
'sniper',
'doubletake',
'pulse_lmg',
'smr',
'softball',
'epg',
'shotgun_pistol',
'wingman_n',
'autopistol',
'semipistol',
'wingman',
'mgl',
'arc_launcher',
'rocket_launcher',
'defender', 'grenade_sonar', 'thermite_grenade', 'grenade_emp', 'smart_pistol', 'satchel', 'grenade_electric_smoke', 'frag_grenade', 'grenade_gravity', 'turretplasma',
]
export async function up(db: Kysely<any>): Promise<void> {
await pgClient.connect()
await Promise.all(weapons.map(async (e) => {
console.log('update ' + e)
await pgClient.query(
`UPDATE KILL SET cause_of_death = 'mp_weapon_${e}' WHERE cause_of_death = '${e}';`
)
await pgClient.query(
`UPDATE KILL SET attacker_current_weapon = 'mp_weapon_${e}' WHERE attacker_current_weapon = '${e}';`
)
await pgClient.query(
`UPDATE KILL SET attacker_weapon_1 = 'mp_weapon_${e}' WHERE attacker_weapon_1 = '${e}';`
)
await pgClient.query(
`UPDATE KILL SET attacker_weapon_2 = 'mp_weapon_${e}' WHERE attacker_weapon_2 = '${e}';`
)
await pgClient.query(
`UPDATE KILL SET attacker_weapon_3 = 'mp_weapon_${e}' WHERE attacker_weapon_3 = '${e}';`
)
await pgClient.query(
`UPDATE KILL SET victim_current_weapon = 'mp_weapon_${e}' WHERE victim_current_weapon = '${e}';`
)
await pgClient.query(
`UPDATE KILL SET victim_weapon_1 = 'mp_weapon_${e}' WHERE victim_weapon_1 = '${e}';`
)
await pgClient.query(
`UPDATE KILL SET victim_weapon_2 = 'mp_weapon_${e}' WHERE victim_weapon_2 = '${e}';`
)
await pgClient.query(
`UPDATE KILL SET victim_weapon_3 = 'mp_weapon_${e}' WHERE victim_weapon_3 = '${e}';`
)
console.log('weapon ' + e + ' updated')
}))
console.log('end')
//Update the hosts column once the hoster table is manually updated
await pgClient.end()
}
export async function down(db: Kysely<any>): Promise<void> { }
@@ -0,0 +1,40 @@
import { Kysely, sql } from 'kysely'
import { Client } from 'pg'
const pgClient = new Client({
host: process.env.POSTGRES_HOST,
database: process.env.POSTGRES_DATABASE,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD
})
export async function up(db: Kysely<any>): Promise<void> {
await pgClient.connect()
await pgClient.query(`ALTER TABLE kill RENAME attacker_offhand_weapon_2 TO attacker_offhand_weapon_2_mods;`)
await pgClient.query(`ALTER TABLE kill RENAME attacker_offhand_weapon_1 TO attacker_offhand_weapon_1_mods;`)
await pgClient.query(`ALTER TABLE kill RENAME victim_offhand_weapon_2 TO victim_offhand_weapon_2_mods;`)
await pgClient.query(`ALTER TABLE kill RENAME victim_offhand_weapon_1 TO victim_offhand_weapon_1_mods;`)
await pgClient.query(`ALTER TABLE kill ADD COLUMN IF NOT EXISTS victim_offhand_weapon_1 character varying NULL;`)
await pgClient.query(`ALTER TABLE kill ADD COLUMN IF NOT EXISTS victim_offhand_weapon_2 character varying NULL;`)
await pgClient.query(`ALTER TABLE kill ADD COLUMN IF NOT EXISTS attacker_offhand_weapon_1 character varying NULL;`)
await pgClient.query(`ALTER TABLE kill ADD COLUMN IF NOT EXISTS attacker_offhand_weapon_2 character varying NULL;`)
//Update the hosts column once the hoster table is manually updated
await pgClient.end()
}
export async function down(db: Kysely<any>): Promise<void> {
await pgClient.connect()
await pgClient.query(`ALTER TABLE kill DROP COLUMN IF EXISTS victim_offhand_weapon_1;`)
await pgClient.query(`ALTER TABLE kill DROP COLUMN IF EXISTS victim_offhand_weapon_2;`)
await pgClient.query(`ALTER TABLE kill DROP COLUMN IF EXISTS attacker_offhand_weapon_1;`)
await pgClient.query(`ALTER TABLE kill DROP COLUMN IF EXISTS attacker_offhand_weapon_2;`)
await pgClient.query(`ALTER TABLE kill RENAME attacker_offhand_weapon_2_mods TO victim_offhand_weapon_2;`)
await pgClient.query(`ALTER TABLE kill RENAME attacker_offhand_weapon_1_mods TO victim_offhand_weapon_1;`)
await pgClient.query(`ALTER TABLE kill RENAME victim_offhand_weapon_2_mods TO victim_offhand_weapon_2;`)
await pgClient.query(`ALTER TABLE kill RENAME victim_offhand_weapon_1_mods TO victim_offhand_weapon_1;`)
await pgClient.end()
}
+9 -4
View File
@@ -21,8 +21,10 @@ export interface KillTable {
attacker_weapon_2_mods: number
attacker_weapon_3: string
attacker_weapon_3_mods: number
attacker_offhand_weapon_1: number
attacker_offhand_weapon_2: number
attacker_offhand_weapon_1: string
attacker_offhand_weapon_1_mods: number
attacker_offhand_weapon_2: string
attacker_offhand_weapon_2_mods: number
victim_name: string
victim_id: string
victim_current_weapon: string
@@ -33,10 +35,13 @@ export interface KillTable {
victim_weapon_2_mods: number
victim_weapon_3: string
victim_weapon_3_mods: string
victim_offhand_weapon_1: number
victim_offhand_weapon_2: number
victim_offhand_weapon_1: string
victim_offhand_weapon_1_mods: number
victim_offhand_weapon_2: string
victim_offhand_weapon_2_mods: number
cause_of_death: string
distance: number
titan?: string
}
/*
interface PlayerTable {
+33 -3
View File
@@ -1,7 +1,7 @@
import { Client } from 'pg'
import { allData } from './process'
const pgClient = new Client({
export const pgClient = new Client({
host: process.env.POSTGRES_HOST,
database: process.env.POSTGRES_DATABASE,
user: process.env.POSTGRES_USER,
@@ -33,12 +33,22 @@ export default async function listenKills() {
e.map == payload.map &&
e.servername == payload.servername
)
let deathWithWeaponEntry = allData.find(
(e) =>
e.attacker_id == payload.victim_id &&
e.cause_of_death == payload.victim_current_weapon &&
e.game_mode == payload.game_mode &&
e.host == payload.host &&
e.map == payload.map &&
e.servername == payload.servername
)
if (!killEntry) {
killEntry = {
kills: 0,
deaths: 0,
max_distance: 0,
total_distance: 0,
deaths_with_weapon: 0,
attacker_name: payload.attacker_name,
attacker_id: payload.attacker_id,
cause_of_death: payload.cause_of_death,
@@ -56,9 +66,10 @@ export default async function listenKills() {
deaths: 0,
max_distance: 0,
total_distance: 0,
deaths_with_weapon: 0,
attacker_name: payload.victim_name,
attacker_id: payload.victim_id,
cause_of_death: payload.victim_current_weapon,
cause_of_death: payload.cause_of_death,
game_mode: payload.game_mode,
host: payload.host,
map: payload.map,
@@ -67,10 +78,29 @@ export default async function listenKills() {
allData.push(deathEntry)
}
if (!deathWithWeaponEntry) {
deathWithWeaponEntry = {
kills: 0,
deaths: 0,
max_distance: 0,
total_distance: 0,
deaths_with_weapon: 0,
attacker_name: payload.victim_name,
attacker_id: payload.victim_id,
cause_of_death: payload.victim_current_weapon,
game_mode: payload.game_mode,
host: payload.host,
map: payload.map,
servername: payload.servername
}
allData.push(deathWithWeaponEntry)
}
killEntry.kills++
killEntry.total_distance += payload.distance
killEntry.max_distance = Math.max(killEntry.max_distance, payload.distance)
killEntry.max_distance = Math.max(killEntry.max_distance || 0, payload.distance)
deathEntry.deaths++
deathWithWeaponEntry.deaths_with_weapon++
})
return pgClient
}
+48 -5
View File
@@ -57,13 +57,33 @@ async function processGlobalStats() {
])
)
.with('deaths', (db) =>
db
.selectFrom('kill')
.select([
count<number>('id').as('deaths'),
'victim_id',
sql<string>`last(victim_name)`.as('victim_name'),
'cause_of_death',
'map',
'game_mode',
'servername',
'host'
])
.groupBy([
'victim_id',
'cause_of_death',
'map',
'servername',
'host',
'game_mode'
])
).with('deathswithweapon', (db) =>
db
.selectFrom('kill')
.select([
count<number>('id').as('deaths'),
'victim_id',
'victim_current_weapon',
sql<string>`last(attacker_name)`.as('attacker_name'),
'map',
'game_mode',
'servername',
@@ -79,16 +99,39 @@ async function processGlobalStats() {
])
)
.selectFrom('kills')
.leftJoin('deaths', (join) =>
.fullJoin('deaths', (join) =>
join
.onRef('deaths.victim_id', '=', 'kills.attacker_id')
.onRef('deaths.victim_current_weapon', '=', 'kills.cause_of_death')
.onRef('deaths.cause_of_death', '=', 'kills.cause_of_death')
.onRef('deaths.servername', '=', 'kills.servername')
.onRef('deaths.host', '=', 'kills.host')
.onRef('deaths.game_mode', '=', 'kills.game_mode')
.onRef('deaths.map', '=', 'kills.map')
)
.selectAll('kills')
.select(sql<number>`COALESCE(deaths.deaths, 0)`.as('deaths'))
.leftJoin('deathswithweapon', (join) =>
join
.onRef('deathswithweapon.victim_id', '=', 'kills.attacker_id')
.onRef('deathswithweapon.victim_current_weapon', '=', 'kills.cause_of_death')
.onRef('deathswithweapon.servername', '=', 'kills.servername')
.onRef('deathswithweapon.host', '=', 'kills.host')
.onRef('deathswithweapon.game_mode', '=', 'kills.game_mode')
.onRef('deathswithweapon.map', '=', 'kills.map')
)
//.selectAll('kills')
.select([
sql<string>`COALESCE(kills.attacker_name, deaths.victim_name)`.as('attacker_name'),
sql<string>`COALESCE(kills.attacker_id, deaths.victim_id)`.as('attacker_id'),
sql<number>`COALESCE(kills.kills, 0)`.as('kills'),
sql<number>`COALESCE(kills.total_distance, 0)`.as('total_distance'),
sql<number>`COALESCE(deathswithweapon.deaths, 0)`.as('deaths_with_weapon'),
sql<number>`COALESCE(kills.max_distance, 0)`.as('max_distance'),
sql<number>`COALESCE(deaths.deaths, 0)`.as('deaths'),
sql<string>`COALESCE(kills.servername, deaths.servername)`.as('servername'),
sql<number>`COALESCE(kills.host, deaths.host)`.as('host'),
sql<string>`COALESCE(kills.cause_of_death, deaths.cause_of_death)`.as('cause_of_death'),
sql<string>`COALESCE(kills.map, deaths.map)`.as('map'),
sql<string>`COALESCE(kills.game_mode, deaths.game_mode)`.as('game_mode'),
])
.execute()
}
export default processAll
+23 -9
View File
@@ -56,14 +56,14 @@ router.post(
'attacker_weapon_1_mods',
'attacker_weapon_2_mods',
'attacker_weapon_3_mods',
'attacker_offhand_weapon_1',
'attacker_offhand_weapon_2',
'attacker_offhand_weapon_1_mods',
'attacker_offhand_weapon_2_mods',
'victim_current_weapon_mods',
'victim_weapon_1_mods',
'victim_weapon_2_mods',
'victim_weapon_3_mods',
'victim_offhand_weapon_1',
'victim_offhand_weapon_2'
'victim_offhand_weapon_1_mods',
'victim_offhand_weapon_2_mods'
]).customSanitizer((value) => {
if (isNaN(value) || !value) {
value = 0
@@ -75,14 +75,14 @@ router.post(
'attacker_weapon_1_mods',
'attacker_weapon_2_mods',
'attacker_weapon_3_mods',
'attacker_offhand_weapon_1',
'attacker_offhand_weapon_2',
'attacker_offhand_weapon_1_mods',
'attacker_offhand_weapon_2_mods',
'victim_current_weapon_mods',
'victim_weapon_1_mods',
'victim_weapon_2_mods',
'victim_weapon_3_mods',
'victim_offhand_weapon_1',
'victim_offhand_weapon_2'
'victim_offhand_weapon_1_mods',
'victim_offhand_weapon_2_mods'
])
.toInt()
.isInt()
@@ -109,12 +109,18 @@ router.post(
'attacker_weapon_1',
'attacker_weapon_2',
'attacker_weapon_3',
'attacker_offhand_weapon_1',
'attacker_offhand_weapon_2',
'victim_name',
'victim_current_weapon',
'victim_weapon_1',
'victim_weapon_2',
'victim_weapon_3',
'cause_of_death'
'victim_offhand_weapon_1',
'victim_offhand_weapon_2',
'cause_of_death',
'victim_titan',
'attacker_titan'
],
'must be composed of a maximum of 50 valid ascii characters'
)
@@ -153,7 +159,9 @@ router.post(
attacker_weapon_3,
attacker_weapon_3_mods,
attacker_offhand_weapon_1,
attacker_offhand_weapon_1_mods,
attacker_offhand_weapon_2,
attacker_offhand_weapon_2_mods,
victim_name,
victim_id,
victim_current_weapon,
@@ -165,7 +173,9 @@ router.post(
victim_weapon_3,
victim_weapon_3_mods,
victim_offhand_weapon_1,
victim_offhand_weapon_1_mods,
victim_offhand_weapon_2,
victim_offhand_weapon_2_mods,
cause_of_death,
distance
} = req.body
@@ -189,7 +199,9 @@ router.post(
attacker_weapon_3,
attacker_weapon_3_mods,
attacker_offhand_weapon_1,
attacker_offhand_weapon_1_mods,
attacker_offhand_weapon_2,
attacker_offhand_weapon_2_mods,
victim_name,
victim_id,
victim_current_weapon,
@@ -201,7 +213,9 @@ router.post(
victim_weapon_3,
victim_weapon_3_mods,
victim_offhand_weapon_1,
victim_offhand_weapon_1_mods,
victim_offhand_weapon_2,
victim_offhand_weapon_2_mods,
cause_of_death,
distance
})
+4 -1
View File
@@ -1,11 +1,13 @@
import { afterAll, beforeAll, describe, expect, test } from '@jest/globals'
import { afterAll, beforeAll, describe, expect, jest, test } from '@jest/globals'
import clientMain from '../src/clientMain'
import { pgClient } from '../src/process/onKill'
import * as dotenv from 'dotenv'
import db from '../src/db/db'
dotenv.config()
let listenServer
jest.setTimeout(30000)
beforeAll(async () => {
listenServer = await clientMain;
})
@@ -59,6 +61,7 @@ describe('client', () => {
afterAll((done) => {
listenServer.close(async () => {
await pgClient.end()
await db.destroy()
done()
})
+33 -16
View File
@@ -1,29 +1,30 @@
import { afterAll, beforeAll, describe, expect, jest, test } from '@jest/globals'
import clientMain from '../src/clientMain'
import serverMain from '../src/serverMain'
import listenKills from "../src/process/onKill"
import { pgClient } from '../src/process/onKill'
import * as dotenv from 'dotenv'
import db from '../src/db/db'
dotenv.config()
let listenClient
let listenServer
let pgClient
const data = {
servername: 'testserver',
attacker_weapon_1_mods: 0,
victim_id: '0',
servername: 'testServer',
victim_name: 'TestVictim',
victim_offhand_weapon_2: 0,
attacker_offhand_weapon_3: '0',
victim_offhand_weapon_2: 'null',
victim_offhand_weapon_2_mods: 0,
victim_weapon_3_mods: 0,
attacker_weapon_2_mods: 0,
attacker_offhand_weapon_1: 0,
attacker_weapon_3_mods: 0,
attacker_offhand_weapon_2: 0,
victim_offhand_weapon_3: '0',
victim_offhand_weapon_1: 0,
attacker_weapon_2_mods: NaN,
attacker_offhand_weapon_1: 'null',
attacker_offhand_weapon_1_mods: 0,
attacker_weapon_3_mods: NaN,
attacker_offhand_weapon_2: 'null',
attacker_offhand_weapon_2_mods: NaN,
victim_offhand_weapon_1: 'offhand_weapon_test',
victim_offhand_weapon_1_mods: 0,
attacker_weapon_3: 'defender',
killstat_version: 'ks_3.0.0',
attacker_weapon_1: 'smr',
@@ -41,11 +42,13 @@ const data = {
game_mode: 'tdm',
map: 'thaw',
attacker_weapon_2: 'autopistol',
victim_weapon_1: 'smr',
victim_weapon_1: 'null',
victim_weapon_2: 'autopistol',
victim_weapon_1_mods: 0,
victim_weapon_3: 'defender',
attacker_name: 'TestAttacker'
attacker_name: 'TestAttacker',
victim_titan: 'null',
attacker_titan: 'null'
}
function waitFor(time: number) {
@@ -54,13 +57,11 @@ function waitFor(time: number) {
})
}
jest.setTimeout(15000)
jest.setTimeout(30000)
beforeAll(async () => {
jest.setTimeout(15000)
listenClient = await clientMain;
listenServer = await serverMain;
pgClient = await listenKills()
const yea = waitFor(1000)
const response = await fetch(`http://127.0.0.1:3001/kill`, {
method: "POST", // *GET, POST, PUT, DELETE, etc.
@@ -79,6 +80,7 @@ describe('realtime', () => {
let playerKills
let weaponKills
let playerDeaths
let serverKills
test('fetch player', async () => {
const request = await fetch("http://127.0.0.1:3000/players")
const data = await request.json()
@@ -100,6 +102,14 @@ describe('realtime', () => {
weaponKills = weapon.kills
})
test('fetch server', async () => {
const request = await fetch("http://127.0.0.1:3000/servers")
const data = await request.json()
const server = data.testserver
expect(server).toHaveProperty('kills')
serverKills = server.kills
})
test('update player', async () => {
jest.setTimeout(15000)
const yea = waitFor(1000)
@@ -130,6 +140,13 @@ describe('realtime', () => {
expect(weapon.kills).toBe(weaponKills + 1)
})
test('check server update', async () => {
const request = await fetch("http://127.0.0.1:3000/servers")
const data = await request.json()
const server = data.testserver
expect(server.kills).toBe(serverKills + 1)
})
})
afterAll((done) => {
+42 -34
View File
@@ -65,15 +65,17 @@ describe('server', () => {
attacker_weapon_1_mods: 0,
victim_id: '0',
victim_name: 'TestVictim',
victim_offhand_weapon_2: 0,
attacker_offhand_weapon_3: '0',
victim_offhand_weapon_2: 'offhand_weapon_test',
victim_offhand_weapon_2_mods: 0,
victim_weapon_3_mods: 0,
attacker_weapon_2_mods: 0,
attacker_offhand_weapon_1: 0,
attacker_offhand_weapon_1: 'offhand_weapon_test',
attacker_offhand_weapon_1_mods: 0,
attacker_weapon_3_mods: 0,
attacker_offhand_weapon_2: 0,
victim_offhand_weapon_3: '0',
victim_offhand_weapon_1: 0,
attacker_offhand_weapon_2: 'offhand_weapon_test',
attacker_offhand_weapon_2_mods: 0,
victim_offhand_weapon_1: 'offhand_weapon_test',
victim_offhand_weapon_1_mods: 0,
attacker_weapon_3: 'defender',
killstat_version: 'ks_3.0.0',
attacker_weapon_1: 'smr',
@@ -95,7 +97,9 @@ describe('server', () => {
victim_weapon_2: 'autopistol',
victim_weapon_1_mods: 0,
victim_weapon_3: 'defender',
attacker_name: 'TestAttacker'
attacker_name: 'TestAttacker',
victim_titan: 'null',
attacker_titan: 'null'
}
const response = await fetch(`http://127.0.0.1:3001/kill`, {
method: "POST", // *GET, POST, PUT, DELETE, etc.
@@ -114,40 +118,44 @@ describe('server', () => {
test('register a kill with missing data', async () => {
const data = {
servername: 'testserver',
attacker_weapon_1_mods: NaN,
attacker_weapon_1_mods: 0,
victim_id: '0',
victim_name: 'TestVictim',
victim_offhand_weapon_2: NaN,
attacker_offhand_weapon_3: 'null',
victim_weapon_3_mods: 19,
victim_offhand_weapon_2: 'null',
victim_offhand_weapon_2_mods: 0,
victim_weapon_3_mods: 0,
attacker_weapon_2_mods: NaN,
attacker_offhand_weapon_1: NaN,
attacker_offhand_weapon_1: 'null',
attacker_offhand_weapon_1_mods: 0,
attacker_weapon_3_mods: NaN,
attacker_offhand_weapon_2: NaN,
victim_offhand_weapon_3: '0',
victim_offhand_weapon_1: 1,
attacker_weapon_3: 'null',
attacker_offhand_weapon_2: 'null',
attacker_offhand_weapon_2_mods: NaN,
victim_offhand_weapon_1: 'offhand_weapon_test',
victim_offhand_weapon_1_mods: 0,
attacker_weapon_3: 'defender',
killstat_version: 'ks_3.0.0',
attacker_weapon_1: 'null',
match_id: '554fa7a3',
distance: 202,
victim_current_weapon: 'car',
cause_of_death: 'satchel',
victim_weapon_2_mods: NaN,
victim_current_weapon_mods: NaN,
attacker_current_weapon_mods: NaN,
game_time: 516.417,
player_count: 13,
attacker_current_weapon: 'null',
attacker_weapon_1: 'smr',
match_id: '31b1f34d',
distance: 0,
victim_current_weapon: 'smr',
cause_of_death: 'smr',
victim_weapon_2_mods: 0,
victim_current_weapon_mods: 0,
attacker_current_weapon_mods: 0,
game_time: 377.799,
player_count: 1,
attacker_current_weapon: 'smr',
attacker_id: '1',
game_mode: 'ps',
map: 'grave',
attacker_weapon_2: 'null',
victim_weapon_1: 'car',
game_mode: 'tdm',
map: 'thaw',
attacker_weapon_2: 'autopistol',
victim_weapon_1: 'null',
victim_weapon_2: 'autopistol',
victim_weapon_1_mods: NaN,
victim_weapon_3: 'arc_launcher',
attacker_name: 'TestAttacker'
victim_weapon_1_mods: 0,
victim_weapon_3: 'defender',
attacker_name: 'TestAttacker',
victim_titan: 'null',
attacker_titan: 'null'
}
const response = await fetch(`http://127.0.0.1:3001/kill`, {
method: "POST", // *GET, POST, PUT, DELETE, etc.