Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
562ccf0060 | ||
|
|
17cb562d82 | ||
|
|
a145baab8b |
@@ -3,8 +3,3 @@ node_modules
|
||||
.env.example
|
||||
npm-debug.log
|
||||
out
|
||||
src/generated
|
||||
coverage
|
||||
.github
|
||||
.vscode
|
||||
.stoplight
|
||||
@@ -1,2 +0,0 @@
|
||||
out
|
||||
.eslintrc.js
|
||||
+15
-29
@@ -1,31 +1,17 @@
|
||||
module.exports = {
|
||||
plugins: ["jest"],
|
||||
"env": {
|
||||
"es2021": true,
|
||||
"node": true,
|
||||
"jest/globals": true
|
||||
},
|
||||
"extends": "standard-with-typescript",
|
||||
"overrides": [
|
||||
{
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"files": [
|
||||
".eslintrc.{js,cjs}"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "script"
|
||||
}
|
||||
}
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module",
|
||||
project: ["./tsconfig.eslint.json"],
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/strict-boolean-expressions": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2021: true
|
||||
},
|
||||
extends: 'standard-with-typescript',
|
||||
overrides: [],
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
project: ['./tsconfig.json']
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/strict-boolean-expressions': 0,
|
||||
'@typescript-eslint/explicit-function-return-type': 'off'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,4 +103,3 @@ dist
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
out/
|
||||
src/generated
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "src/db"]
|
||||
path = src/db
|
||||
url = https://github.com/ToneAPI/backend-model
|
||||
Vendored
+20
-2
@@ -10,8 +10,26 @@
|
||||
"name": "Launch Server API",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}/src/serverMain.ts",
|
||||
"preLaunchTask": "npm: build",
|
||||
"outFiles": ["${workspaceFolder}/out/**/*.js"],
|
||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||
"outFiles": ["${workspaceFolder}/out/**/*.js"]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Client API",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}/src/clientMain.ts",
|
||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||
"outFiles": ["${workspaceFolder}/out/**/*.js"]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch Websocket",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}/src/websocket.ts",
|
||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||
"outFiles": ["${workspaceFolder}/out/**/*.js"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+1
-2
@@ -6,6 +6,5 @@
|
||||
"eslint.format.enable": true,
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-25
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "typescript",
|
||||
"tsconfig": "tsconfig.json",
|
||||
"problemMatcher": ["$tsc"],
|
||||
"group": "build",
|
||||
"label": "tsc: build - tsconfig.json"
|
||||
},
|
||||
{
|
||||
"label": "myShellCommand",
|
||||
"type": "shell",
|
||||
"command": "echo goodfood"
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "build",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"label": "npm: build",
|
||||
"detail": "npm run generate && npx tsc"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -13,24 +13,3 @@ The serverside and clientside parts are two different executables. You may need
|
||||
An OpenAPI file is located in `docs` directory.
|
||||
|
||||
Alternatively, you can find it converted at https://toneapi.github.io/ToneAPI_backend/
|
||||
|
||||
|
||||
# V3 Roadmap
|
||||
### Server mod
|
||||
- [x] Match registration
|
||||
- [x] Player stats
|
||||
- [x] Match stats
|
||||
- [x] Weapon shots, hits, crits, ricochets
|
||||
- [X] Weapon time
|
||||
- [ ] Grenade shots
|
||||
- [ ] Notify that a player has connected
|
||||
|
||||
### Backend
|
||||
- [x] Match registration
|
||||
- [x] Kill stats
|
||||
- [X] Match stats
|
||||
- [ ] Titan not being registered in loadout
|
||||
- [ ] Titan weapons not being registered in loadout
|
||||
|
||||
### Frontend
|
||||
- [ ] Everything
|
||||
-1
Submodule docs deleted from 4c24c6525f
+465
File diff suppressed because one or more lines are too long
+592
@@ -0,0 +1,592 @@
|
||||
openapi: 3.0.2
|
||||
info:
|
||||
title: ToneAPI
|
||||
version: '1.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 '
|
||||
servers:
|
||||
- url: 'https://tone.sleepycat.date/v1'
|
||||
paths:
|
||||
/client/servers:
|
||||
get:
|
||||
summary: List of servers
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
title: Server
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
example: 1
|
||||
name:
|
||||
type: string
|
||||
example: fvnknoots 7v7
|
||||
examples:
|
||||
Example 1:
|
||||
value:
|
||||
- id: 1
|
||||
name: fvnknoots 7v7
|
||||
operationId: get-server-list
|
||||
description: An array containing the list of servers as objects
|
||||
parameters: []
|
||||
/client/weapons:
|
||||
get:
|
||||
summary: Statistics for all weapons
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
hemlock:
|
||||
title: Weapon
|
||||
type: object
|
||||
properties:
|
||||
max_kill_distance:
|
||||
type: integer
|
||||
avg_kill_distance:
|
||||
type: number
|
||||
kills:
|
||||
type: integer
|
||||
examples:
|
||||
Example 1:
|
||||
value:
|
||||
hemlock:
|
||||
max_kill_distance: 0
|
||||
avg_kill_distance: 0
|
||||
kills: 0
|
||||
lstar:
|
||||
max_kill_distance: 0
|
||||
avg_kill_distance: 0
|
||||
kills: 0
|
||||
operationId: get-client-weapons
|
||||
parameters:
|
||||
- name: server
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
example: 2
|
||||
description: Fetch data for specific server
|
||||
- name: player
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
example: 1005930844007
|
||||
description: Fetch data for specific player
|
||||
requestBody:
|
||||
content: {}
|
||||
description: A JSON Object with weapon IDS as keys and weapon data as value
|
||||
description: A JSON Object with weapon IDs as key and weapon data as value
|
||||
parameters: []
|
||||
'/client/weapons/{weaponId}':
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
example: epg
|
||||
name: weaponId
|
||||
in: path
|
||||
required: true
|
||||
description: ID of a weapon
|
||||
get:
|
||||
summary: Statistics for a single weapon
|
||||
operationId: get-client-weapons-weaponId
|
||||
requestBody:
|
||||
content: {}
|
||||
parameters:
|
||||
- name: server
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
example: 2
|
||||
description: Fetch data for specific server
|
||||
- name: player
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
example: 1005930844007
|
||||
description: Fetch data for specific player
|
||||
description: Data for a specific weapon
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
title: Weapon
|
||||
type: object
|
||||
properties:
|
||||
max_kill_distance:
|
||||
type: integer
|
||||
x-stoplight:
|
||||
id: npckmqv4zdoje
|
||||
avg_kill_distance:
|
||||
type: number
|
||||
x-stoplight:
|
||||
id: 8plgr96gp1ke8
|
||||
kills:
|
||||
type: integer
|
||||
x-stoplight:
|
||||
id: pha6hu1qjfsgk
|
||||
/client/players:
|
||||
get:
|
||||
summary: Statistics for all players
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
'2250125460':
|
||||
title: Player
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: Legonzaur
|
||||
deaths:
|
||||
type: integer
|
||||
kills:
|
||||
type: integer
|
||||
max_kill_distance:
|
||||
type: integer
|
||||
avg_kill_distance:
|
||||
type: number
|
||||
examples:
|
||||
Example 1:
|
||||
value:
|
||||
'2250125460':
|
||||
name: Legonzaur
|
||||
deaths: 0
|
||||
kills: 0
|
||||
max_kill_distance: 0
|
||||
avg_kill_distance: 0
|
||||
operationId: get-client-players
|
||||
parameters:
|
||||
- name: weapon
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
example: epg
|
||||
description: Fetch data for specific weapon
|
||||
- name: server
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
example: 2
|
||||
description: Fetch data for specific server
|
||||
description: A JSON Object with player IDs as key and player data as value
|
||||
'/client/players/{playerId}':
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
name: playerId
|
||||
in: path
|
||||
required: true
|
||||
description: ID of a player
|
||||
get:
|
||||
summary: Statistics for a single player
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
title: Player
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: Legonzaur
|
||||
deaths:
|
||||
type: integer
|
||||
x-stoplight:
|
||||
id: xhi261df7kcqo
|
||||
kills:
|
||||
type: integer
|
||||
x-stoplight:
|
||||
id: ofjpw5sevnf64
|
||||
max_kill_distance:
|
||||
type: integer
|
||||
x-stoplight:
|
||||
id: qjqm0t2n6o93p
|
||||
avg_kill_distance:
|
||||
type: number
|
||||
x-stoplight:
|
||||
id: y0i5i1eja7v84
|
||||
operationId: get-client-players-playerId
|
||||
parameters:
|
||||
- name: server
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
example: 2
|
||||
description: Fetch data for specific server
|
||||
- name: weapon
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
example: epg
|
||||
description: Fetch data for specific weapon
|
||||
description: Data for a specific player
|
||||
'/servers/{serverId}':
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
example: '2'
|
||||
name: serverId
|
||||
in: path
|
||||
required: true
|
||||
description: ID of the server
|
||||
post:
|
||||
summary: ''
|
||||
operationId: post-servers-serverId
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'403':
|
||||
description: Forbidden
|
||||
description: Used to test auth
|
||||
security:
|
||||
- Auth_Token: []
|
||||
'/servers/{serverId}/kill':
|
||||
post:
|
||||
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:
|
||||
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
|
||||
description: ''
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
example: '2'
|
||||
name: serverId
|
||||
in: path
|
||||
required: true
|
||||
description: ID of the server
|
||||
components:
|
||||
schemas:
|
||||
Player:
|
||||
title: Player
|
||||
x-stoplight:
|
||||
id: c9lv0bwq7hdx7
|
||||
type: object
|
||||
x-examples:
|
||||
Example 1:
|
||||
name: Legonzaur
|
||||
deaths: 0
|
||||
kills: 0
|
||||
max_kill_distance: 0
|
||||
avg_kill_distance: 0
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
example: Legonzaur
|
||||
deaths:
|
||||
type: integer
|
||||
x-stoplight:
|
||||
id: xhi261df7kcqo
|
||||
kills:
|
||||
type: integer
|
||||
x-stoplight:
|
||||
id: ofjpw5sevnf64
|
||||
max_kill_distance:
|
||||
type: integer
|
||||
x-stoplight:
|
||||
id: qjqm0t2n6o93p
|
||||
avg_kill_distance:
|
||||
type: number
|
||||
x-stoplight:
|
||||
id: y0i5i1eja7v84
|
||||
Weapon:
|
||||
title: Weapon
|
||||
x-stoplight:
|
||||
id: l8j4cdsuxxrx0
|
||||
type: object
|
||||
x-examples:
|
||||
Example 1:
|
||||
max_kill_distance: 0
|
||||
avg_kill_distance: 0
|
||||
kills: 0
|
||||
properties:
|
||||
max_kill_distance:
|
||||
type: integer
|
||||
x-stoplight:
|
||||
id: npckmqv4zdoje
|
||||
avg_kill_distance:
|
||||
type: number
|
||||
x-stoplight:
|
||||
id: 8plgr96gp1ke8
|
||||
kills:
|
||||
type: integer
|
||||
x-stoplight:
|
||||
id: pha6hu1qjfsgk
|
||||
Server:
|
||||
title: Server
|
||||
x-stoplight:
|
||||
id: g45919da0b3rc
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
example: 1
|
||||
name:
|
||||
type: string
|
||||
example: fvnknoots 7v7
|
||||
Kill:
|
||||
title: Kill
|
||||
x-stoplight:
|
||||
id: 8w7qpo5dz7fo9
|
||||
type: object
|
||||
x-examples:
|
||||
Example 1:
|
||||
id: 0
|
||||
server: string
|
||||
tone_version: ks_3.0.0
|
||||
match_id: 1cde15ee
|
||||
game_mode: tdm
|
||||
map: string
|
||||
game_time: 385.417
|
||||
player_count: 1
|
||||
attacker_name: Legonzaur
|
||||
attacker_id: 0
|
||||
attacker_current_weapon: rspn101
|
||||
attacker_current_weapon_mods: 0
|
||||
attacker_weapon_1: rspn101
|
||||
attacker_weapon_1_mods: 0
|
||||
attacker_weapon_2: autopistol
|
||||
attacker_weapon_2_mods: 0
|
||||
attacker_weapon_3: defender
|
||||
attacker_weapon_3_mods: 0
|
||||
attacker_offhand_weapon_1: 0
|
||||
attacker_offhand_weapon_2: 8
|
||||
victim_name: Legonzaur
|
||||
victim_id: 0
|
||||
victim_current_weapon: rspn101
|
||||
victim_current_weapon_mods: 0
|
||||
victim_weapon_1: rspn101
|
||||
victim_weapon_1_mods: 0
|
||||
victim_weapon_2: autopistol
|
||||
victim_weapon_2_mods: 0
|
||||
victim_weapon_3: defender
|
||||
victim_weapon_3_mods: 0
|
||||
victim_offhand_weapon_1: 0
|
||||
victim_offhand_weapon_2: 8
|
||||
cause_of_death: frag_grenade
|
||||
distance: 0
|
||||
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: basic
|
||||
parameters:
|
||||
server:
|
||||
name: server
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
example: 2
|
||||
description: Fetch data for specific server
|
||||
player:
|
||||
name: player
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: number
|
||||
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
|
||||
responses: {}
|
||||
examples: {}
|
||||
+453
@@ -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://toneapi.ovh/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://toneapi.ovh/v2'
|
||||
paths:
|
||||
/client/hosts:
|
||||
get:
|
||||
summary: List of hosts
|
||||
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: Statistics for all maps
|
||||
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: Statistics for all gamemodes
|
||||
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
-5
@@ -65,11 +65,7 @@ export default {
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
isolatedModules: true
|
||||
}
|
||||
}
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
Generated
+2422
-4944
File diff suppressed because it is too large
Load Diff
+11
-12
@@ -10,7 +10,6 @@
|
||||
"pg": "^8.9.0",
|
||||
"redis": "^4.6.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"typia": "^4.2.1",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -21,23 +20,23 @@
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/pg": "^8.6.6",
|
||||
"@typescript-eslint/eslint-plugin": "^6.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
||||
"babel-jest": "^29.4.3",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-standard-with-typescript": "^39.0.0",
|
||||
"eslint-plugin-import": "^2.28.1",
|
||||
"eslint-plugin-jest": "^27.2.3",
|
||||
"eslint-plugin-n": "^16.0.2",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-config-standard-with-typescript": "^34.0.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-n": "^15.6.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"jest": "^29.4.3",
|
||||
"typescript": "^5.2.2"
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"main": "out/index.js",
|
||||
"scripts": {
|
||||
"build": "npm run generate && npx tsc",
|
||||
"build": "npx tsc",
|
||||
"startServer": "node out/serverMain.js",
|
||||
"documentation": "redocly build-docs .\\docs\\v3.yml --output=docs\\index.html",
|
||||
"test": "jest --runInBand",
|
||||
"generate": "npx typia generate --input src/templates --output src/generated --project tsconfig.json"
|
||||
"startClient": "node out/clientMain.js",
|
||||
"startWebsocket": "node out/websocket.js",
|
||||
"documentation": "redocly build-docs .\\docs\\v2.yml --output=docs\\index.html",
|
||||
"test": "jest --runInBand"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Router } from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { validateErrors } from '../common'
|
||||
import db, { getHostList } from '../db/db'
|
||||
import { sql } from 'kysely'
|
||||
import { allData } from '../process/process'
|
||||
|
||||
const { max, sum } = db.fn
|
||||
const router = Router()
|
||||
// timeout middleware ?
|
||||
router.get('/*', (req, res, next) => {
|
||||
next()
|
||||
})
|
||||
|
||||
const queryFilters = {
|
||||
player: 'attacker_id',
|
||||
server: 'servername',
|
||||
map: 'map',
|
||||
weapon: 'cause_of_death',
|
||||
gamemode: 'game_mode',
|
||||
host: 'host'
|
||||
} as const
|
||||
|
||||
const path = {
|
||||
player: 'attacker_id',
|
||||
servers: 'servername',
|
||||
maps: 'map',
|
||||
weapons: 'cause_of_death',
|
||||
gamemodes: 'game_mode',
|
||||
hosts: 'host'
|
||||
} as const
|
||||
|
||||
interface KillRecord {
|
||||
kills: number
|
||||
deaths: number
|
||||
deaths_while_equipped: number
|
||||
username?: string
|
||||
max_distance: number
|
||||
total_distance: number
|
||||
}
|
||||
|
||||
router.get('/hosts', (_req, res) => {
|
||||
void (async () => {
|
||||
const result = await getHostList()
|
||||
const data: Record<number, string> = {}
|
||||
result.forEach((e) => (data[Number(e.id)] = e.name))
|
||||
|
||||
const dataString = JSON.stringify(data)
|
||||
const buffer = Buffer.from(dataString)
|
||||
const size = buffer.length
|
||||
res.status(200).setHeader('X-File-Size', size).setHeader('Content-Type', 'application/json').send(buffer)
|
||||
})()
|
||||
})
|
||||
|
||||
function processQueryArgs (data: ReturnType<typeof db.selectFrom<'kill_view'>>, queryArgs: string | string[]): ReturnType<typeof db.selectFrom<'kill_view'>> {
|
||||
Object.entries(queryArgs).forEach(([one, two]) => {
|
||||
if (!two) return
|
||||
if (!(one in queryFilters)) return
|
||||
const key = one as keyof typeof queryFilters
|
||||
if (Array.isArray(two)) {
|
||||
data = data.where((qb) => {
|
||||
(two as string[]).forEach((e) => { qb = qb.orWhere(queryFilters[key], '=', e) })
|
||||
return qb
|
||||
})
|
||||
} else {
|
||||
data = data.where(queryFilters[key], '=', two)
|
||||
}
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
router.get('/players',
|
||||
(req, res) => {
|
||||
void (async () => {
|
||||
const result = db.selectFrom('kill_view')
|
||||
let data
|
||||
const resultFiltered = processQueryArgs(result, req.query as unknown as string | string[])
|
||||
// Cache stuff when no route is present so request doesn't takes 4secs to complete
|
||||
if (result !== resultFiltered) {
|
||||
const selection = resultFiltered.select([sum<number>('kills').as('kills'), sum<number>('deaths').as('deaths'), sum<number>('deaths_with_weapon').as('deaths_while_equipped'), 'attacker_id', sql<string>`last(attacker_name)`.as('username'), sum<number>('total_distance').as('total_distance'), max('max_distance').as('max_distance')]).groupBy('attacker_id')
|
||||
data = (await selection.execute()).reduce<Record<string, KillRecord>>((acc, curr) => {
|
||||
acc[curr.attacker_id] = {
|
||||
kills: Number(curr.kills),
|
||||
deaths: Number(curr.deaths),
|
||||
max_distance: Number(curr.max_distance),
|
||||
total_distance: Number(curr.total_distance),
|
||||
deaths_while_equipped: Number(curr.deaths_while_equipped),
|
||||
username: curr.username
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
} else {
|
||||
data = allData
|
||||
}
|
||||
|
||||
const dataString = JSON.stringify(data)
|
||||
const buffer = Buffer.from(dataString)
|
||||
const size = buffer.length
|
||||
res.status(200).setHeader('X-File-Size', size).setHeader('Content-Type', 'application/json').send(buffer)
|
||||
})()
|
||||
})
|
||||
|
||||
router.get('/:dataType',
|
||||
param('dataType')
|
||||
.custom((e) => e in path)
|
||||
.withMessage('Only weapons, players, maps, gamemodes or servers are valid paths'),
|
||||
validateErrors,
|
||||
(req, res) => {
|
||||
void (async () => {
|
||||
let result = db.selectFrom('kill_view')
|
||||
if (req.query) {
|
||||
result = processQueryArgs(result, req.query as unknown as string | string[])
|
||||
}
|
||||
const dataType = req.params.dataType as keyof typeof path
|
||||
|
||||
const selection = result.select([sum<number>('kills').as('kills'), sum<number>('deaths').as('deaths'), sum<number>('deaths_with_weapon').as('deaths_while_equipped'), path[dataType], sum<number>('total_distance').as('total_distance'), max('max_distance').as('max_distance')]).groupBy(path[dataType])
|
||||
const data = (await selection.execute()).reduce<Record<string, KillRecord>>((acc, curr) => {
|
||||
acc[curr[path[dataType]]] = {
|
||||
kills: Number(curr.kills),
|
||||
deaths: Number(curr.deaths),
|
||||
max_distance: Number(curr.max_distance),
|
||||
total_distance: Number(curr.total_distance),
|
||||
deaths_while_equipped: Number(curr.deaths_while_equipped)
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
const dataString = JSON.stringify(data)
|
||||
const buffer = Buffer.from(dataString)
|
||||
const size = buffer.length
|
||||
res.status(200).setHeader('X-File-Size', size).setHeader('Content-Type', 'application/json').send(buffer)
|
||||
})()
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as dotenv from 'dotenv'
|
||||
import cluster from 'cluster'
|
||||
import os from 'os'
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import client from './client/client'
|
||||
import { dbReady } from './db/db'
|
||||
import processAll from './process/process'
|
||||
import listenKills from './process/onKill'
|
||||
dotenv.config()
|
||||
|
||||
const cCPUs = os.cpus().length
|
||||
|
||||
const port = 3000
|
||||
|
||||
if (cluster.isPrimary && process.env.ENVIRONMENT !== 'dev') {
|
||||
// Create a worker for each CPU
|
||||
for (let i = 0; i < cCPUs; i++) {
|
||||
cluster.fork()
|
||||
}
|
||||
cluster.on('online', function (worker) {
|
||||
console.log(`Worker ${worker.process.pid ?? ''} is online`)
|
||||
})
|
||||
cluster.on('exit', function (worker, code, signal) {
|
||||
console.log(`Worker ${worker.process.pid ?? ''} died`)
|
||||
})
|
||||
}
|
||||
|
||||
export default
|
||||
new Promise((resolve, reject) => {
|
||||
void (async () => {
|
||||
if (!cluster.isPrimary || process.env.ENVIRONMENT === 'dev') {
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
app.use(cors({ exposedHeaders: ['X-File-Size', 'Content-Encoding'] }))
|
||||
app.get('/', (req, res) => {
|
||||
res.send('Tone client API online')
|
||||
})
|
||||
|
||||
app.use('/', client)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
})()
|
||||
})
|
||||
-1
Submodule src/db deleted from b8a684acfa
@@ -0,0 +1,62 @@
|
||||
import * as dotenv from 'dotenv'
|
||||
import { Kysely, PostgresDialect } from 'kysely'
|
||||
import { Pool } from 'pg'
|
||||
import migrateToLatest from './migrations'
|
||||
import { type KillTable } from './model'
|
||||
import type Database from './model'
|
||||
dotenv.config()
|
||||
const migration = migrateToLatest()
|
||||
const db = new Kysely<Database>({
|
||||
dialect: new PostgresDialect({
|
||||
pool: new Pool({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
database: process.env.POSTGRES_DATABASE,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
max: 30
|
||||
})
|
||||
}),
|
||||
log (event) {
|
||||
if (process.env.ENVIRONMENT !== 'dev') {
|
||||
return
|
||||
}
|
||||
if (event.level === 'query') {
|
||||
console.log(event.query.sql)
|
||||
console.log(event.query.parameters)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
interface RemoveFromKill {
|
||||
id: unknown
|
||||
unix_time: unknown
|
||||
}
|
||||
|
||||
type KillRecord = Omit<KillTable, keyof RemoveFromKill>
|
||||
|
||||
export async function CreateKillRecord (data: KillRecord) {
|
||||
await db
|
||||
.insertInto('kill')
|
||||
.values({ ...data })
|
||||
.execute()
|
||||
}
|
||||
export async function getHostList () {
|
||||
return await db.selectFrom('host').select(['id', 'host.name']).execute()
|
||||
}
|
||||
// tokens are stored in raw... maybe we should use something better in the future
|
||||
// Using callback for express-basic-auth
|
||||
export async function CheckServerToken (token: string) {
|
||||
return await db.selectFrom('host').select('id')
|
||||
.where('token', '=', Buffer.from(token, 'base64').toString())
|
||||
.executeTakeFirst()
|
||||
.then((result) => {
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
export async function dbReady (): Promise<Kysely<Database>> {
|
||||
await migration
|
||||
return db
|
||||
}
|
||||
|
||||
export default db
|
||||
@@ -0,0 +1,52 @@
|
||||
import { promises as fs } from "fs";
|
||||
import {
|
||||
FileMigrationProvider,
|
||||
Kysely,
|
||||
Migrator,
|
||||
PostgresDialect,
|
||||
} from "kysely";
|
||||
import * as path from "path";
|
||||
import { Pool } from "pg";
|
||||
import Database from "./model";
|
||||
|
||||
async function migrateToLatest() {
|
||||
const db = new Kysely<Database>({
|
||||
dialect: new PostgresDialect({
|
||||
pool: new Pool({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
database: process.env.POSTGRES_DATABASE,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const migrator = new Migrator({
|
||||
db,
|
||||
provider: new FileMigrationProvider({
|
||||
fs,
|
||||
path,
|
||||
migrationFolder: path.join(__dirname, "./migrations"),
|
||||
}),
|
||||
});
|
||||
|
||||
const { error, results } = await migrator.migrateToLatest();
|
||||
|
||||
results?.forEach((it) => {
|
||||
if (it.status === "Success") {
|
||||
console.log(`migration "${it.migrationName}" was executed successfully`);
|
||||
} else if (it.status === "Error") {
|
||||
console.error(`failed to execute migration "${it.migrationName}"`);
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("failed to migrate");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
}
|
||||
|
||||
export default migrateToLatest;
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Kysely, sql } from 'kysely'
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable('player')
|
||||
.addColumn('id', 'integer', (col) => col.unique().primaryKey())
|
||||
.addColumn('name', 'varchar')
|
||||
.addColumn('optout', 'boolean')
|
||||
.addColumn('hide_TOS', 'boolean')
|
||||
.execute()
|
||||
|
||||
await db.schema
|
||||
.createTable('weapon')
|
||||
.addColumn('id', 'varchar', (col) => col.unique().primaryKey())
|
||||
.addColumn('name', 'varchar')
|
||||
.addColumn('description', 'varchar')
|
||||
.addColumn('image', 'varchar')
|
||||
.execute()
|
||||
|
||||
await db.schema
|
||||
.createTable('map')
|
||||
.addColumn('id', 'varchar', (col) => col.unique().primaryKey())
|
||||
.addColumn('name', 'varchar')
|
||||
.addColumn('description', 'varchar')
|
||||
.addColumn('image', 'varchar')
|
||||
.execute()
|
||||
|
||||
await db.schema
|
||||
.createTable('server')
|
||||
.addColumn('id', 'serial', (col) => col.primaryKey())
|
||||
.addColumn('name', 'varchar', (col) => col.unique())
|
||||
.addColumn('description', 'varchar')
|
||||
.addColumn('token', 'varchar', (col) =>
|
||||
col.notNull().defaultTo(sql`gen_random_uuid()`)
|
||||
)
|
||||
.execute()
|
||||
|
||||
await db.schema
|
||||
.createTable('kill')
|
||||
.addColumn('id', 'serial', (col) => col.primaryKey())
|
||||
.addColumn('server', 'integer', (col) =>
|
||||
col.references('server.id').notNull().onDelete('cascade')
|
||||
)
|
||||
.addColumn('killstat_version', 'varchar')
|
||||
.addColumn('match_id', 'varchar')
|
||||
.addColumn('game_mode', 'varchar')
|
||||
.addColumn('map', 'varchar')
|
||||
.addColumn('unix_time', 'timestamp', (col) =>
|
||||
col.defaultTo(sql`now()`).notNull()
|
||||
)
|
||||
.addColumn('game_time', 'decimal')
|
||||
.addColumn('player_count', 'integer')
|
||||
.addColumn('attacker_name', 'varchar')
|
||||
.addColumn('attacker_id', 'varchar')
|
||||
.addColumn('attacker_current_weapon', 'varchar')
|
||||
.addColumn('attacker_current_weapon_mods', 'integer')
|
||||
.addColumn('attacker_weapon_1', 'varchar')
|
||||
.addColumn('attacker_weapon_1_mods', 'integer')
|
||||
.addColumn('attacker_weapon_2', 'varchar')
|
||||
.addColumn('attacker_weapon_2_mods', 'integer')
|
||||
.addColumn('attacker_weapon_3', 'varchar')
|
||||
.addColumn('attacker_weapon_3_mods', 'integer')
|
||||
.addColumn('attacker_offhand_weapon_1', 'integer')
|
||||
.addColumn('attacker_offhand_weapon_2', 'integer')
|
||||
.addColumn('victim_name', 'varchar')
|
||||
.addColumn('victim_id', 'varchar')
|
||||
.addColumn('victim_current_weapon', 'varchar')
|
||||
.addColumn('victim_current_weapon_mods', 'integer')
|
||||
.addColumn('victim_weapon_1', 'varchar')
|
||||
.addColumn('victim_weapon_1_mods', 'integer')
|
||||
.addColumn('victim_weapon_2', 'varchar')
|
||||
.addColumn('victim_weapon_2_mods', 'integer')
|
||||
.addColumn('victim_weapon_3', 'varchar')
|
||||
.addColumn('victim_weapon_3_mods', 'integer')
|
||||
.addColumn('victim_offhand_weapon_1', 'integer')
|
||||
.addColumn('victim_offhand_weapon_2', 'integer')
|
||||
.addColumn('cause_of_death', 'varchar')
|
||||
.addColumn('distance', 'decimal')
|
||||
.execute()
|
||||
|
||||
/*await db.schema
|
||||
.createTable("pet")
|
||||
.addColumn("id", "serial", (col) => col.primaryKey())
|
||||
.addColumn("name", "varchar", (col) => col.notNull().unique())
|
||||
.addColumn("owner_id", "integer", (col) =>
|
||||
col.references("person.id").onDelete("cascade").notNull()
|
||||
)
|
||||
.addColumn("species", "varchar", (col) => col.notNull())
|
||||
.execute();*/
|
||||
/*
|
||||
await db.schema
|
||||
.createIndex("kill")
|
||||
.on("pet")
|
||||
.column("owner_id")
|
||||
.execute();*/
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable('kill').execute()
|
||||
await db.schema.dropTable('player').execute()
|
||||
await db.schema.dropTable('weapon').execute()
|
||||
await db.schema.dropTable('map').execute()
|
||||
await db.schema.dropTable('server').execute()
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Kysely, sql } from 'kysely'
|
||||
import { Client } from 'pg'
|
||||
|
||||
const notify_newkill = `
|
||||
CREATE OR REPLACE FUNCTION notify_new_kill()
|
||||
RETURNS trigger
|
||||
AS
|
||||
$$
|
||||
BEGIN
|
||||
PERFORM pg_notify('new_kill', row_to_json(NEW)::text);
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$
|
||||
LANGUAGE plpgsql;
|
||||
`
|
||||
|
||||
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(notify_newkill)
|
||||
await pgClient.query(`CREATE TRIGGER insert_kills_notify AFTER INSERT ON kill FOR EACH ROW EXECUTE PROCEDURE notify_new_kill();`)
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await pgClient.query(`DROP FUNCTION IF EXISTS notify_new_kill;`)
|
||||
await pgClient.query(`DROP TRIGGER IF EXISTS insert_kills_notify;`)
|
||||
await pgClient.end()
|
||||
}
|
||||
@@ -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('DROP TABLE IF EXISTS player')
|
||||
|
||||
await pgClient.query('ALTER TABLE kill ADD COLUMN IF NOT EXISTS servername character varying NULL;')
|
||||
await pgClient.query('ALTER TABLE kill ADD COLUMN IF NOT EXISTS host integer NULL;')
|
||||
await pgClient.query('UPDATE kill SET servername = server.name FROM server WHERE server.id = kill.server;')
|
||||
|
||||
await pgClient.query('CREATE TABLE host (id SERIAL PRIMARY KEY, name character varying, token character varying)')
|
||||
await pgClient.query('INSERT INTO host (token) SELECT DISTINCT token FROM server')
|
||||
await pgClient.query('UPDATE kill SET host = host.id FROM host FULL JOIN server ON server.token = host.token WHERE host.token = server.token AND server.id = kill.server;')
|
||||
|
||||
await pgClient.query('ALTER TABLE kill ALTER COLUMN servername SET NOT NULL;')
|
||||
await pgClient.query('ALTER TABLE kill DROP COLUMN server;')
|
||||
await pgClient.query('DROP TABLE IF EXISTS server')
|
||||
//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 ADD server integer NULL;')
|
||||
await pgClient.query(`ALTER TABLE kill ADD server integer NULL;`)
|
||||
await pgClient.query('UPDATE kill SET server = server.id FROM server where server.name = kill.servername;')
|
||||
await pgClient.query('ALTER TABLE kill ALTER COLUMN server SET NOT NULL;')
|
||||
await pgClient.query('ALTER TABLE kill DROP COLUMN servername;')
|
||||
await pgClient.query('ALTER TABLE kill DROP COLUMN host;')
|
||||
await pgClient.query('DROP TABLE IF EXISTS host')
|
||||
await pgClient.end()
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
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(`CREATE OR REPLACE FUNCTION public.first_agg (anyelement, anyelement)
|
||||
RETURNS anyelement
|
||||
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS
|
||||
'SELECT $1';`)
|
||||
|
||||
await pgClient.query(`CREATE AGGREGATE public.first (anyelement) (
|
||||
SFUNC = public.first_agg
|
||||
, STYPE = anyelement
|
||||
, PARALLEL = safe
|
||||
);`)
|
||||
|
||||
await pgClient.query(`CREATE OR REPLACE FUNCTION public.last_agg (anyelement, anyelement)
|
||||
RETURNS anyelement
|
||||
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS
|
||||
'SELECT $2';`)
|
||||
|
||||
|
||||
await pgClient.query(`CREATE AGGREGATE public.last (anyelement) (
|
||||
SFUNC = public.last_agg
|
||||
, STYPE = anyelement
|
||||
, PARALLEL = safe
|
||||
);`)
|
||||
//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('DROP AGGREGATE public.last')
|
||||
pgClient.query('DROP FUNCTION public.last_agg')
|
||||
pgClient.query('DROP AGGREGATE public.first')
|
||||
pgClient.query('DROP AGGREGATE public.first_agg')
|
||||
await pgClient.end()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
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 = {
|
||||
titan_sword: 'melee_titan_sword',
|
||||
yh803_bullet: 'mp_weapon_yh803_bullet',
|
||||
titan_punch_scorch: 'melee_titan_punch_scorch',
|
||||
titan_punch_tone: 'melee_titan_punch_tone',
|
||||
titan_punch_ion: "melee_titan_punch_ion",
|
||||
titan_punch_vanguard: "melee_titan_punch_vanguard",
|
||||
titan_punch_legion: "melee_titan_punch_legion",
|
||||
titan_punch_northstar: "melee_titan_punch_northstar",
|
||||
}
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await pgClient.connect()
|
||||
await Promise.all(
|
||||
Object.entries(weapons).map(async (e) => {
|
||||
console.log('update ' + e[0])
|
||||
await pgClient.query(
|
||||
`UPDATE KILL SET cause_of_death = '${e[1]}' WHERE cause_of_death = '${e[0]}';`
|
||||
)
|
||||
await pgClient.query(
|
||||
`UPDATE KILL SET attacker_current_weapon = '${e[1]}' WHERE attacker_current_weapon = '${e[0]}';`
|
||||
)
|
||||
await pgClient.query(
|
||||
`UPDATE KILL SET attacker_weapon_1 = '${e[1]}' WHERE attacker_weapon_1 = '${e[0]}';`
|
||||
)
|
||||
await pgClient.query(
|
||||
`UPDATE KILL SET attacker_weapon_2 = '${e[1]}' WHERE attacker_weapon_2 = '${e[0]}';`
|
||||
)
|
||||
await pgClient.query(
|
||||
`UPDATE KILL SET attacker_weapon_3 = '${e[1]}' WHERE attacker_weapon_3 = '${e[0]}';`
|
||||
)
|
||||
await pgClient.query(
|
||||
`UPDATE KILL SET victim_current_weapon = '${e[1]}' WHERE victim_current_weapon = '${e[0]}';`
|
||||
)
|
||||
await pgClient.query(
|
||||
`UPDATE KILL SET victim_weapon_1 = '${e[1]}' WHERE victim_weapon_1 = '${e[0]}';`
|
||||
)
|
||||
await pgClient.query(
|
||||
`UPDATE KILL SET victim_weapon_2 = '${e[1]}' WHERE victim_weapon_2 = '${e[0]}';`
|
||||
)
|
||||
await pgClient.query(
|
||||
`UPDATE KILL SET victim_weapon_3 = '${e[1]}' WHERE victim_weapon_3 = '${e[0]}';`
|
||||
)
|
||||
console.log('weapon ' + e[1] + ' 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,126 @@
|
||||
import { type Kysely } 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('DROP TABLE IF EXISTS kill_view;')
|
||||
console.log('creating table kill_view')
|
||||
await pgClient.query(`CREATE TABLE kill_view
|
||||
AS (
|
||||
with "kills" as (select count("id") as "kills", coalesce(max("distance"), 0) as "max_distance", coalesce(sum("distance"), 0) as "total_distance", "attacker_id", last(attacker_name) as "attacker_name", "cause_of_death", "map", "game_mode", "servername", "host" from "kill" where "attacker_id" != "victim_id" group by "attacker_id", "cause_of_death", "map", "servername", "host", "game_mode"), "deaths" as (select count("id") as "deaths", "victim_id", last(victim_name) as "victim_name", "cause_of_death", "map", "game_mode", "servername", "host" from "kill" group by "victim_id", "cause_of_death", "map", "servername", "host", "game_mode"), "deathswithweapon" as (select count("id") as "deaths", "victim_id", "victim_current_weapon", "map", "game_mode", "servername", "host" from "kill" group by "victim_id", "victim_current_weapon", "map", "servername", "host", "game_mode") select COALESCE(kills.attacker_name, deaths.victim_name) as "attacker_name", COALESCE(kills.attacker_id, deaths.victim_id) as "attacker_id", COALESCE(kills.kills, 0) as "kills", COALESCE(kills.total_distance, 0) as "total_distance", COALESCE(deathswithweapon.deaths, 0) as "deaths_with_weapon", COALESCE(kills.max_distance, 0) as "max_distance", COALESCE(deaths.deaths, 0) as "deaths", COALESCE(kills.servername, deaths.servername) as "servername", COALESCE(kills.host, deaths.host) as "host", COALESCE(kills.cause_of_death, deaths.cause_of_death) as "cause_of_death", COALESCE(kills.map, deaths.map) as "map", COALESCE(kills.game_mode, deaths.game_mode) as "game_mode" from "kills" full join "deaths" on "deaths"."victim_id" = "kills"."attacker_id" and "deaths"."cause_of_death" = "kills"."cause_of_death" and "deaths"."servername" = "kills"."servername" and "deaths"."host" = "kills"."host" and "deaths"."game_mode" = "kills"."game_mode" and "deaths"."map" = "kills"."map" left join "deathswithweapon" on "deathswithweapon"."victim_id" = "kills"."attacker_id" and "deathswithweapon"."victim_current_weapon" = "kills"."cause_of_death" and "deathswithweapon"."servername" = "kills"."servername" and "deathswithweapon"."host" = "kills"."host" and "deathswithweapon"."game_mode" = "kills"."game_mode" and "deathswithweapon"."map" = "kills"."map"
|
||||
);`)
|
||||
console.log('creating index on attacker_id')
|
||||
await pgClient.query('CREATE INDEX ON kill_view USING HASH ("attacker_id");')
|
||||
console.log('creating index on map')
|
||||
await pgClient.query('CREATE INDEX ON kill_view USING HASH ("map");')
|
||||
console.log('creating index on game_mode')
|
||||
await pgClient.query('CREATE INDEX ON kill_view USING HASH ("game_mode");')
|
||||
console.log('creating index on cause_of_death')
|
||||
await pgClient.query('CREATE INDEX ON kill_view USING HASH ("cause_of_death");')
|
||||
console.log('creating index on server')
|
||||
await pgClient.query('CREATE INDEX ON kill_view ("servername", "host");')
|
||||
console.log('creating function update_kill_view_fnct')
|
||||
await pgClient.query(`CREATE OR REPLACE FUNCTION update_kill_view_fnct()
|
||||
RETURNS TRIGGER
|
||||
AS $$
|
||||
BEGIN
|
||||
IF EXISTS(
|
||||
SELECT * FROM kill_view
|
||||
WHERE
|
||||
new.attacker_id = attacker_id AND
|
||||
new.map = map AND
|
||||
new.game_mode = game_mode AND
|
||||
new.cause_of_death = cause_of_death AND
|
||||
new.servername = servername AND
|
||||
new.host = host
|
||||
) THEN
|
||||
UPDATE kill_view SET kills = kills + 1,
|
||||
max_distance = (SELECT Max(v) FROM (VALUES (new.distance), (max_distance)) AS value(v)),
|
||||
total_distance = total_distance + new.distance,
|
||||
attacker_name = new.attacker_name
|
||||
WHERE
|
||||
new.attacker_id = attacker_id AND
|
||||
new.map = map AND
|
||||
new.game_mode = game_mode AND
|
||||
new.cause_of_death = cause_of_death AND
|
||||
new.servername = servername AND
|
||||
new.host = host;
|
||||
ELSE
|
||||
INSERT INTO kill_view(kills,deaths, deaths_with_weapon, attacker_id, map, game_mode, cause_of_death, servername, host, max_distance, total_distance)
|
||||
VALUES(1,0,0, new.attacker_id, new.map, new.game_mode, new.cause_of_death, new.servername, new.host,new.distance, new.distance);
|
||||
END IF;
|
||||
|
||||
|
||||
IF EXISTS(
|
||||
SELECT * FROM kill_view
|
||||
WHERE
|
||||
new.victim_id = attacker_id AND
|
||||
new.map = map AND
|
||||
new.game_mode = game_mode AND
|
||||
new.cause_of_death = cause_of_death AND
|
||||
new.servername = servername AND
|
||||
new.host = host
|
||||
) THEN
|
||||
UPDATE kill_view SET deaths = deaths + 1
|
||||
WHERE
|
||||
new.victim_id = attacker_id AND
|
||||
new.map = map AND
|
||||
new.game_mode = game_mode AND
|
||||
new.cause_of_death = cause_of_death AND
|
||||
new.servername = servername AND
|
||||
new.host = host;
|
||||
ELSE
|
||||
INSERT INTO kill_view(deaths, kills, deaths_with_weapon, attacker_id, map, game_mode, cause_of_death, servername, host, max_distance, total_distance)
|
||||
VALUES(1,0,0, new.victim_id, new.map, new.game_mode, new.cause_of_death, new.servername, new.host,0,0);
|
||||
END IF;
|
||||
|
||||
|
||||
IF EXISTS(
|
||||
SELECT * FROM kill_view
|
||||
WHERE
|
||||
new.victim_id = attacker_id AND
|
||||
new.map = map AND
|
||||
new.game_mode = game_mode AND
|
||||
new.attacker_current_weapon = cause_of_death AND
|
||||
new.servername = servername AND
|
||||
new.host = host
|
||||
) THEN
|
||||
UPDATE kill_view SET deaths_with_weapon = deaths_with_weapon + 1
|
||||
WHERE
|
||||
new.victim_id = attacker_id AND
|
||||
new.map = map AND
|
||||
new.game_mode = game_mode AND
|
||||
new.attacker_current_weapon = cause_of_death AND
|
||||
new.servername = servername AND
|
||||
new.host = host;
|
||||
ELSE
|
||||
INSERT INTO kill_view(deaths, kills, deaths_with_weapon, attacker_id, map, game_mode, cause_of_death, servername, host, max_distance, total_distance)
|
||||
VALUES(0,0,1, new.victim_id, new.map, new.game_mode, new.attacker_current_weapon, new.servername, new.host,0,0);
|
||||
END IF;
|
||||
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE PLPGSQL;`)
|
||||
console.log('creating trigger update_kill_view')
|
||||
await pgClient.query(`CREATE OR REPLACE TRIGGER update_kill_view
|
||||
AFTER INSERT ON kill
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_kill_view_fnct();`)
|
||||
await pgClient.end()
|
||||
}
|
||||
|
||||
export async function down (db: Kysely<any>): Promise<void> {
|
||||
await pgClient.connect()
|
||||
await pgClient.query('DROP TRIGGER IF EXISTS update_kill_view;')
|
||||
await pgClient.query('DROP FUNCTION IF EXISTS update_kill_view_fnct')
|
||||
await pgClient.query('DROP TABLE IF EXISTS kill_view;')
|
||||
await pgClient.end()
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { type Kysely } 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(`CREATE OR REPLACE FUNCTION update_kill_view_fnct()
|
||||
RETURNS TRIGGER
|
||||
AS $$
|
||||
BEGIN
|
||||
IF EXISTS(
|
||||
SELECT * FROM kill_view
|
||||
WHERE
|
||||
new.attacker_id = attacker_id AND
|
||||
new.map = map AND
|
||||
new.game_mode = game_mode AND
|
||||
new.cause_of_death = cause_of_death AND
|
||||
new.servername = servername AND
|
||||
new.host = host
|
||||
) THEN
|
||||
UPDATE kill_view SET kills = kills + 1,
|
||||
max_distance = (SELECT Max(v) FROM (VALUES (new.distance), (max_distance)) AS value(v)),
|
||||
total_distance = total_distance + new.distance,
|
||||
attacker_name = new.attacker_name
|
||||
WHERE
|
||||
new.attacker_id = attacker_id AND
|
||||
new.map = map AND
|
||||
new.game_mode = game_mode AND
|
||||
new.cause_of_death = cause_of_death AND
|
||||
new.servername = servername AND
|
||||
new.host = host;
|
||||
ELSE
|
||||
INSERT INTO kill_view(kills,deaths, deaths_with_weapon, attacker_id, map, game_mode, cause_of_death, servername, host, max_distance, total_distance, attacker_name)
|
||||
VALUES(1,0,0, new.attacker_id, new.map, new.game_mode, new.cause_of_death, new.servername, new.host,new.distance, new.distance, new.attacker_name);
|
||||
END IF;
|
||||
|
||||
|
||||
IF EXISTS(
|
||||
SELECT * FROM kill_view
|
||||
WHERE
|
||||
new.victim_id = attacker_id AND
|
||||
new.map = map AND
|
||||
new.game_mode = game_mode AND
|
||||
new.cause_of_death = cause_of_death AND
|
||||
new.servername = servername AND
|
||||
new.host = host
|
||||
) THEN
|
||||
UPDATE kill_view SET deaths = deaths + 1
|
||||
WHERE
|
||||
new.victim_id = attacker_id AND
|
||||
new.map = map AND
|
||||
new.game_mode = game_mode AND
|
||||
new.cause_of_death = cause_of_death AND
|
||||
new.servername = servername AND
|
||||
new.host = host;
|
||||
ELSE
|
||||
INSERT INTO kill_view(deaths, kills, deaths_with_weapon, attacker_id, map, game_mode, cause_of_death, servername, host, max_distance, total_distance, attacker_name)
|
||||
VALUES(1,0,0, new.victim_id, new.map, new.game_mode, new.cause_of_death, new.servername, new.host,0,0, new.victim_name);
|
||||
END IF;
|
||||
|
||||
|
||||
IF EXISTS(
|
||||
SELECT * FROM kill_view
|
||||
WHERE
|
||||
new.victim_id = attacker_id AND
|
||||
new.map = map AND
|
||||
new.game_mode = game_mode AND
|
||||
new.attacker_current_weapon = cause_of_death AND
|
||||
new.servername = servername AND
|
||||
new.host = host
|
||||
) THEN
|
||||
UPDATE kill_view SET deaths_with_weapon = deaths_with_weapon + 1
|
||||
WHERE
|
||||
new.victim_id = attacker_id AND
|
||||
new.map = map AND
|
||||
new.game_mode = game_mode AND
|
||||
new.attacker_current_weapon = cause_of_death AND
|
||||
new.servername = servername AND
|
||||
new.host = host;
|
||||
ELSE
|
||||
INSERT INTO kill_view(deaths, kills, deaths_with_weapon, attacker_id, map, game_mode, cause_of_death, servername, host, max_distance, total_distance, attacker_name)
|
||||
VALUES(0,0,1, new.victim_id, new.map, new.game_mode, new.attacker_current_weapon, new.servername, new.host,0,0, new.victim_name);
|
||||
END IF;
|
||||
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE PLPGSQL;`)
|
||||
await pgClient.end()
|
||||
}
|
||||
|
||||
export async function down (db: Kysely<any>): Promise<void> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { type Generated } from 'kysely'
|
||||
|
||||
export interface KillTable {
|
||||
id: Generated<number>
|
||||
servername: string
|
||||
host: number
|
||||
killstat_version: string
|
||||
match_id: string
|
||||
game_mode: string
|
||||
map: string
|
||||
unix_time: Generated<Date>
|
||||
game_time: number
|
||||
player_count: number
|
||||
attacker_name: string
|
||||
attacker_id: string
|
||||
attacker_current_weapon: string
|
||||
attacker_current_weapon_mods: number
|
||||
attacker_weapon_1: string
|
||||
attacker_weapon_1_mods: number
|
||||
attacker_weapon_2: string
|
||||
attacker_weapon_2_mods: number
|
||||
attacker_weapon_3: string
|
||||
attacker_weapon_3_mods: 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
|
||||
victim_current_weapon_mods: number
|
||||
victim_weapon_1: string
|
||||
victim_weapon_1_mods: number
|
||||
victim_weapon_2: string
|
||||
victim_weapon_2_mods: number
|
||||
victim_weapon_3: string
|
||||
victim_weapon_3_mods: string
|
||||
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
|
||||
}
|
||||
|
||||
export interface KillViewTable {
|
||||
kills: number
|
||||
deaths: number
|
||||
deaths_with_weapon: number
|
||||
attacker_id: number
|
||||
attacker_name: string
|
||||
map: string
|
||||
game_mode: string
|
||||
cause_of_death: string
|
||||
servername: string
|
||||
host: number
|
||||
max_distance: number
|
||||
total_distance: number
|
||||
}
|
||||
|
||||
interface HosterTable {
|
||||
id: Generated<number>
|
||||
name: string
|
||||
token: Generated<string>
|
||||
}
|
||||
interface Database {
|
||||
kill_view: KillViewTable
|
||||
kill: KillTable
|
||||
host: HosterTable
|
||||
}
|
||||
|
||||
export default Database
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Client } from 'pg'
|
||||
import { allData } from './process'
|
||||
|
||||
export 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 default async function listenKills () {
|
||||
await pgClient.connect()
|
||||
await pgClient.query('LISTEN new_kill')
|
||||
|
||||
pgClient.on('notification', (data) => {
|
||||
if (!data.payload) return
|
||||
const payload = JSON.parse(data.payload)
|
||||
let killEntry = allData[payload.attacker_id]
|
||||
let deathEntry = allData[payload.victim_id]
|
||||
|
||||
if (!killEntry) {
|
||||
killEntry = {
|
||||
kills: 0,
|
||||
deaths: 0,
|
||||
max_distance: 0,
|
||||
total_distance: 0,
|
||||
deaths_while_equipped: 0,
|
||||
username: payload.attacker_name
|
||||
}
|
||||
allData[payload.attacker_id] = killEntry
|
||||
}
|
||||
|
||||
if (!deathEntry) {
|
||||
deathEntry = {
|
||||
kills: 0,
|
||||
deaths: 0,
|
||||
max_distance: 0,
|
||||
total_distance: 0,
|
||||
deaths_while_equipped: 0,
|
||||
username: payload.attacker_name
|
||||
}
|
||||
allData[payload.victim_id] = deathEntry
|
||||
}
|
||||
|
||||
if (payload.victim_id !== payload.attacker_id) {
|
||||
killEntry.kills++
|
||||
killEntry.total_distance = Number(killEntry.total_distance) + Number(payload.distance)
|
||||
killEntry.max_distance = Math.max(killEntry.max_distance || 0, payload.distance)
|
||||
}
|
||||
deathEntry.deaths++
|
||||
deathEntry.deaths_while_equipped++
|
||||
deathEntry.username = payload.victim_name
|
||||
killEntry.username = payload.attacker_name
|
||||
})
|
||||
return pgClient
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import db from '../db/db'
|
||||
import { sql } from 'kysely'
|
||||
const { max, sum } = db.fn
|
||||
|
||||
export let allData: Awaited<ReturnType<typeof processGlobalStats>>
|
||||
|
||||
/**
|
||||
* populates allData
|
||||
* @returns
|
||||
*/
|
||||
async function processAll (): Promise<void> {
|
||||
console.log('Starting data calculation...')
|
||||
const timeStart = new Date()
|
||||
|
||||
allData = await processGlobalStats()
|
||||
|
||||
console.log(`Data calculation finished. Took + ${Math.abs(new Date().getTime() - timeStart.getTime()) / 1000} seconds`)
|
||||
}
|
||||
|
||||
interface KillRecord {
|
||||
kills: number
|
||||
deaths: number
|
||||
deaths_while_equipped: number
|
||||
username?: string
|
||||
max_distance: number
|
||||
total_distance: number
|
||||
}
|
||||
|
||||
async function processGlobalStats () {
|
||||
const data = await db.selectFrom('kill_view').select(['attacker_id', sql<string>`last(attacker_name)`.as('username'), sum('kills').as('kills'), sum('deaths').as('deaths'), sum('deaths_with_weapon').as('deaths_while_equipped'), sum('total_distance').as('total_distance'), max('max_distance').as('max_distance')]).groupBy('attacker_id').execute()
|
||||
return data.reduce<Record<string, KillRecord>>((acc, curr) => {
|
||||
acc[curr.attacker_id] = {
|
||||
kills: Number(curr.kills),
|
||||
deaths: Number(curr.deaths),
|
||||
max_distance: Number(curr.max_distance),
|
||||
total_distance: Number(curr.total_distance),
|
||||
deaths_while_equipped: Number(curr.deaths_while_equipped),
|
||||
username: curr.username
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
export default processAll
|
||||
@@ -0,0 +1,282 @@
|
||||
import { Router } from 'express'
|
||||
import { body, header } from 'express-validator'
|
||||
import { CreateKillRecord, CheckServerToken } from '../db/db'
|
||||
import { validateErrors } from '../common'
|
||||
|
||||
const router = Router()
|
||||
|
||||
//auth middleware
|
||||
router.post(
|
||||
'/*',
|
||||
header('authorization')
|
||||
.exists({ checkFalsy: true })
|
||||
.withMessage('Missing Authorization Header')
|
||||
.bail()
|
||||
.custom(e => e.split(' ')[0].toLowerCase() == 'bearer')
|
||||
.withMessage('Authorization Token is not Bearer'),
|
||||
validateErrors,
|
||||
async (req, res, next) => {
|
||||
if (!req) return res.sendStatus(500)
|
||||
if (!req.headers.authorization) {
|
||||
console.error("no authorization header")
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
const query = await CheckServerToken(req.headers.authorization.split(' ')[1])
|
||||
if (!query || !query.id) {
|
||||
console.error("incorrect token : " + req.headers.authorization.split(' ')[1] + " for " + req.body.servername + " with IP " + (req.headers['x-forwarded-for']?.toString() ||
|
||||
req.socket.remoteAddress?.toString() ||
|
||||
''))
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
next()
|
||||
}
|
||||
)
|
||||
|
||||
//Route to check auth
|
||||
router.post('/', (req, res) => {
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
const serversCount: { [id: string]: number } = {}
|
||||
const serversTimeout: { [id: string]: NodeJS.Timeout } = {}
|
||||
|
||||
//same rate limiting code as register. max 10 kills per server every 1 sec. should be enough.
|
||||
/*router.post('/kill', (req, res, next) => {
|
||||
let host = Number(req.query.serverId)
|
||||
if (serversCount[serverId] > 2) {
|
||||
return res.status(429).json({
|
||||
error: 'too many requests. Are players really making that much kills ?'
|
||||
})
|
||||
}
|
||||
clearTimeout(serversTimeout[serverId])
|
||||
serversCount[serverId] =
|
||||
serversCount[serverId] ?? (serversCount[serverId] + 1) | 1
|
||||
serversTimeout[serverId] = setTimeout(() => {
|
||||
serversCount[serverId] = 0
|
||||
}, 1000)
|
||||
next()
|
||||
})*/
|
||||
|
||||
router.post(
|
||||
'/kill',
|
||||
body([
|
||||
'attacker_current_weapon_mods',
|
||||
'attacker_weapon_1_mods',
|
||||
'attacker_weapon_2_mods',
|
||||
'attacker_weapon_3_mods',
|
||||
'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_mods',
|
||||
'victim_offhand_weapon_2_mods'
|
||||
]).customSanitizer((value) => {
|
||||
if (isNaN(value) || !value) {
|
||||
value = 0
|
||||
}
|
||||
return value
|
||||
}),
|
||||
body([
|
||||
'attacker_current_weapon_mods',
|
||||
'attacker_weapon_1_mods',
|
||||
'attacker_weapon_2_mods',
|
||||
'attacker_weapon_3_mods',
|
||||
'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_mods',
|
||||
'victim_offhand_weapon_2_mods'
|
||||
])
|
||||
.toInt()
|
||||
.isInt()
|
||||
.withMessage('must be a valid int'),
|
||||
body(['distance', 'player_count'])
|
||||
.toInt()
|
||||
.isInt()
|
||||
.withMessage('must be a valid int'),
|
||||
body(['distance', 'game_time'])
|
||||
.toFloat()
|
||||
.isFloat()
|
||||
.withMessage('must be a valid float'),
|
||||
body(
|
||||
[
|
||||
'attacker_id',
|
||||
'victim_id',
|
||||
'killstat_version',
|
||||
'match_id',
|
||||
'game_mode',
|
||||
'map',
|
||||
'attacker_name',
|
||||
'attacker_current_weapon',
|
||||
'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',
|
||||
'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'
|
||||
)
|
||||
.isString()
|
||||
.isLength({ max: 50 })
|
||||
.isAscii(),
|
||||
body('servername',).isString()
|
||||
.isLength({ max: 100 })
|
||||
.isAscii(),
|
||||
body(['distance', 'game_time'], 'must be postitive floats').isFloat({
|
||||
min: 0
|
||||
}),
|
||||
body(['cause_of_death', 'victim_id'], 'mandatory').exists().notEmpty(),
|
||||
//do we need this ?
|
||||
//body('servername').customSanitizer(e => e.replace(/[^a-z0-9]/gi, '')),
|
||||
validateErrors,
|
||||
async (req, res) => {
|
||||
// Do we check the same thing twice ?????
|
||||
if (!req.headers.authorization) return res.sendStatus(403)
|
||||
const headers = req.headers.authorization.split(' ')
|
||||
if (headers[0].toLowerCase() != "bearer") return res.status(403).send("authorization must be token bearer")
|
||||
const query = (await CheckServerToken(headers[1]))
|
||||
if (!query) return res.sendStatus(403)
|
||||
const host = query.id
|
||||
const {
|
||||
servername,
|
||||
killstat_version,
|
||||
match_id,
|
||||
game_mode,
|
||||
map,
|
||||
game_time,
|
||||
player_count,
|
||||
attacker_name,
|
||||
attacker_id,
|
||||
attacker_current_weapon,
|
||||
attacker_current_weapon_mods,
|
||||
attacker_weapon_1,
|
||||
attacker_weapon_1_mods,
|
||||
attacker_weapon_2,
|
||||
attacker_weapon_2_mods,
|
||||
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,
|
||||
victim_current_weapon_mods,
|
||||
victim_weapon_1,
|
||||
victim_weapon_1_mods,
|
||||
victim_weapon_2,
|
||||
victim_weapon_2_mods,
|
||||
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
|
||||
CreateKillRecord({
|
||||
killstat_version,
|
||||
servername,
|
||||
host,
|
||||
match_id,
|
||||
game_mode,
|
||||
map,
|
||||
game_time,
|
||||
player_count,
|
||||
attacker_name,
|
||||
attacker_id,
|
||||
attacker_current_weapon,
|
||||
attacker_current_weapon_mods,
|
||||
attacker_weapon_1,
|
||||
attacker_weapon_1_mods,
|
||||
attacker_weapon_2,
|
||||
attacker_weapon_2_mods,
|
||||
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,
|
||||
victim_current_weapon_mods,
|
||||
victim_weapon_1,
|
||||
victim_weapon_1_mods,
|
||||
victim_weapon_2,
|
||||
victim_weapon_2_mods,
|
||||
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
|
||||
})
|
||||
.then((e) => {
|
||||
res.sendStatus(201)
|
||||
console.log(
|
||||
`[${new Date().toLocaleString()}] Kill submitted for server ${servername}, ${attacker_name} killed ${victim_name}`
|
||||
)
|
||||
})
|
||||
.catch((e) => {
|
||||
res.sendStatus(500)
|
||||
console.error({
|
||||
killstat_version,
|
||||
servername,
|
||||
host,
|
||||
match_id,
|
||||
game_mode,
|
||||
map,
|
||||
game_time,
|
||||
player_count,
|
||||
attacker_name,
|
||||
attacker_id,
|
||||
attacker_current_weapon,
|
||||
attacker_current_weapon_mods,
|
||||
attacker_weapon_1,
|
||||
attacker_weapon_1_mods,
|
||||
attacker_weapon_2,
|
||||
attacker_weapon_2_mods,
|
||||
attacker_weapon_3,
|
||||
attacker_weapon_3_mods,
|
||||
attacker_offhand_weapon_1,
|
||||
attacker_offhand_weapon_2,
|
||||
victim_name,
|
||||
victim_id,
|
||||
victim_current_weapon,
|
||||
victim_current_weapon_mods,
|
||||
victim_weapon_1,
|
||||
victim_weapon_1_mods,
|
||||
victim_weapon_2,
|
||||
victim_weapon_2_mods,
|
||||
victim_weapon_3,
|
||||
victim_weapon_3_mods,
|
||||
victim_offhand_weapon_1,
|
||||
victim_offhand_weapon_2,
|
||||
cause_of_death,
|
||||
distance
|
||||
})
|
||||
console.error(e)
|
||||
})
|
||||
}
|
||||
)
|
||||
export default router
|
||||
+5
-12
@@ -1,11 +1,9 @@
|
||||
// Typed routes from https://urosstok.com/blog/typed-routes-in-express
|
||||
|
||||
import * as dotenv from 'dotenv'
|
||||
import express, { type ErrorRequestHandler } from 'express'
|
||||
import cors from 'cors'
|
||||
import { dbReady } from './db'
|
||||
import server from './generated/server'
|
||||
dotenv.config()
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import db, { dbReady } from './db/db'
|
||||
import server from './server/server'
|
||||
|
||||
const app = express()
|
||||
const port = 3001
|
||||
@@ -18,14 +16,9 @@ app.get('/', (req, res) => {
|
||||
})
|
||||
|
||||
app.use('/', server)
|
||||
const errorHandler: ErrorRequestHandler = (err, req, res, next) => {
|
||||
res.status(500).send({ error: [{ msg: 'Internal Error! You\'d better report this' }] })
|
||||
console.error(err)
|
||||
}
|
||||
app.use(errorHandler)
|
||||
|
||||
export default new Promise((resolve, reject) => {
|
||||
void dbReady().then((e) => {
|
||||
dbReady().then((e) => {
|
||||
const listenServer = app.listen(port, '0.0.0.0', () => {
|
||||
console.log(`Tone server api listening on port ${port}`)
|
||||
resolve(listenServer)
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { Router, type Response } from 'express'
|
||||
import {
|
||||
checkOrCreateLoadout,
|
||||
checkOrCreateTitan,
|
||||
checkOrCreateWeapon
|
||||
} from '../utils'
|
||||
import { type KillData, validateBody, type RequestBody } from '../types'
|
||||
import db from '../db'
|
||||
import typia from 'typia'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
validateBody(typia.createValidate<KillData>()),
|
||||
(req: RequestBody<KillData>, res: Response) => {
|
||||
void (async () => {
|
||||
const host_id = res.locals.host_id
|
||||
const startTime = Date.now()
|
||||
const {
|
||||
game_time,
|
||||
distance,
|
||||
match_id,
|
||||
attacker: attackerData,
|
||||
victim: victimData,
|
||||
cause_of_death
|
||||
} = req.body
|
||||
const attacker_id = Number(attackerData.id)
|
||||
const victim_id = Number(victimData.id)
|
||||
if (isNaN(attacker_id) || isNaN(victim_id)) {
|
||||
res
|
||||
.status(400)
|
||||
.send({ errors: [{ msg: 'attacker_id or victim_id is NaN' }] })
|
||||
return
|
||||
}
|
||||
const match = await db
|
||||
.selectFrom('ToneAPI_v3.match')
|
||||
.select(['ToneAPI_v3.match.ongoing', 'ToneAPI_v3.match.match_id'])
|
||||
.where('ToneAPI_v3.match.host_id', '=', host_id)
|
||||
.where('ToneAPI_v3.match.match_id', '=', match_id)
|
||||
.executeTakeFirst()
|
||||
if (!match) {
|
||||
res.status(403).send({
|
||||
errors: [
|
||||
{
|
||||
msg: 'Match does not exists',
|
||||
param: 'match_id',
|
||||
location: 'body'
|
||||
}
|
||||
]
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!match.ongoing) {
|
||||
res.status(409).send({
|
||||
errors: [
|
||||
{
|
||||
msg: 'Match is already closed',
|
||||
param: 'match_id',
|
||||
location: 'body'
|
||||
}
|
||||
]
|
||||
})
|
||||
return
|
||||
}
|
||||
// await checkUpdateOrCreatePlayer(
|
||||
// { id: attacker_id, name: attackerData.name })
|
||||
// await checkUpdateOrCreatePlayer(
|
||||
// { id: victim_id, name: victimData.name }
|
||||
// )
|
||||
|
||||
await Promise.all([
|
||||
checkOrCreateWeapon(cause_of_death),
|
||||
checkOrCreateWeapon(attackerData.current_weapon.id),
|
||||
checkOrCreateWeapon(victimData.current_weapon.id),
|
||||
async () => { if (attackerData.loadout.titan) { await checkOrCreateTitan(attackerData.loadout.titan) } },
|
||||
async () => { if (victimData.loadout.titan) { await checkOrCreateTitan(victimData.loadout.titan) } }
|
||||
])
|
||||
|
||||
const [attacker_loadout, victim_loadout] = await Promise.all([
|
||||
checkOrCreateLoadout(attackerData.loadout),
|
||||
checkOrCreateLoadout(victimData.loadout)
|
||||
])
|
||||
|
||||
const insertResult = await db
|
||||
.insertInto('ToneAPI_v3.kill')
|
||||
.values({
|
||||
attacker_id,
|
||||
victim_id,
|
||||
match_id,
|
||||
attacker_loadout_id: attacker_loadout,
|
||||
victim_loadout_id: victim_loadout,
|
||||
attacker_speed: attackerData.velocity,
|
||||
victim_speed: victimData.velocity,
|
||||
attacker_movementstate: attackerData.state,
|
||||
victim_movementstate: victimData.state,
|
||||
distance,
|
||||
game_time,
|
||||
cause_of_death,
|
||||
attacker_held_weapon: attackerData.current_weapon.id,
|
||||
victim_held_weapon: victimData.current_weapon.id
|
||||
})
|
||||
.returning('kill_id')
|
||||
.executeTakeFirstOrThrow()
|
||||
|
||||
res.status(201).send({ id: insertResult.kill_id })
|
||||
console.log((Date.now() - startTime) + ' ms')
|
||||
})()
|
||||
}
|
||||
)
|
||||
|
||||
export default router
|
||||
@@ -1,171 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { Router, type Response } from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { validateErrors } from '../common'
|
||||
import { type MatchData, type MatchCloseData, validateBody, type RequestBody } from '../types'
|
||||
import db from '../db'
|
||||
import typia from 'typia'
|
||||
import { checkOrCreateTitan, checkOrCreateWeapon } from '../utils'
|
||||
import { type TitanStatsInMatchTable, type WeaponStatsInMatchTable } from '../db/model'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
validateBody(typia.createValidate<MatchData>()),
|
||||
(req: RequestBody<MatchData>, res: Response) => {
|
||||
void (async () => {
|
||||
const host_id = res.locals.host_id
|
||||
const { air_accel, server_name, game_map, gamemode } = req.body
|
||||
const server = await db
|
||||
.selectFrom('ToneAPI_v3.server')
|
||||
.selectAll()
|
||||
.where('ToneAPI_v3.server.host_id', '=', host_id)
|
||||
.where('ToneAPI_v3.server.server_name', '=', server_name)
|
||||
.executeTakeFirst()
|
||||
if (!server) {
|
||||
await db
|
||||
.insertInto('ToneAPI_v3.server')
|
||||
.values({ host_id, server_name })
|
||||
.execute()
|
||||
}
|
||||
|
||||
await db
|
||||
.updateTable('ToneAPI_v3.match')
|
||||
.set({ ongoing: false })
|
||||
.where('ToneAPI_v3.match.server_name', '=', server_name)
|
||||
.execute()
|
||||
|
||||
const match = await db
|
||||
.insertInto('ToneAPI_v3.match')
|
||||
.values({
|
||||
air_accel,
|
||||
server_name,
|
||||
game_map,
|
||||
gamemode,
|
||||
host_id,
|
||||
ongoing: true
|
||||
})
|
||||
.returning('ToneAPI_v3.match.match_id')
|
||||
.executeTakeFirstOrThrow()
|
||||
|
||||
res.status(201).send({ match: match?.match_id })
|
||||
})()
|
||||
}
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/:match_id/close',
|
||||
param('match_id').exists().withMessage('Missing Match ID').bail().isNumeric().withMessage('Match ID is not numeric'),
|
||||
validateErrors,
|
||||
validateBody(typia.createValidate<MatchCloseData>()),
|
||||
(req: RequestBody<MatchCloseData>, res: Response) => {
|
||||
void (async () => {
|
||||
const host_id = res.locals.host_id
|
||||
const match_id = req.params.match_id
|
||||
const match = await db
|
||||
.selectFrom('ToneAPI_v3.match')
|
||||
.selectAll()
|
||||
.where('ToneAPI_v3.match.host_id', '=', host_id)
|
||||
.where('ToneAPI_v3.match.match_id', '=', match_id)
|
||||
.executeTakeFirst()
|
||||
if (!match) {
|
||||
res.status(403).send({
|
||||
errors: [
|
||||
{
|
||||
msg: 'Match does not exists for this host',
|
||||
param: 'match_id',
|
||||
location: 'param'
|
||||
}
|
||||
]
|
||||
})
|
||||
return
|
||||
}
|
||||
const playerPromises: Array<Promise<any>> = []
|
||||
for (const playerId in req.body) {
|
||||
const NumPlayerId = Number(playerId)
|
||||
if (isNaN(NumPlayerId)) {
|
||||
res
|
||||
.status(400)
|
||||
.send({ errors: [{ msg: 'playerId is NaN', location: 'body' }] })
|
||||
return
|
||||
}
|
||||
const playerData = req.body[playerId]
|
||||
|
||||
const promises: Array<Promise<any>> = []
|
||||
const weaponStats: WeaponStatsInMatchTable[] = []
|
||||
const titanStats: TitanStatsInMatchTable[] = []
|
||||
for (const weaponId in playerData.weapons) {
|
||||
promises.push(
|
||||
checkOrCreateWeapon(weaponId)
|
||||
.then(async () => {
|
||||
weaponStats.push({
|
||||
match_id: match.match_id,
|
||||
weapon_id: weaponId,
|
||||
player_id: NumPlayerId,
|
||||
headshots: playerData.weapons[weaponId].shotsHeadshot,
|
||||
playtime: playerData.weapons[weaponId].playtime,
|
||||
ricochets: playerData.weapons[weaponId].shotsRichochet,
|
||||
shots_fired: playerData.weapons[weaponId].shotsFired,
|
||||
shots_hit: playerData.weapons[weaponId].shotsHit
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
for (const titanId in playerData.titans) {
|
||||
promises.push(
|
||||
checkOrCreateTitan(titanId).then(async () => {
|
||||
titanStats.push({
|
||||
match_id: match.match_id,
|
||||
titan_id: titanId,
|
||||
player_id: NumPlayerId,
|
||||
headshots: playerData.titans[titanId].shotsHeadshot,
|
||||
playtime: playerData.titans[titanId].playtime,
|
||||
ricochets: playerData.titans[titanId].shotsRichochet,
|
||||
shots_fired: playerData.titans[titanId].shotsFired,
|
||||
shots_hit: playerData.titans[titanId].shotsHit
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
playerPromises.push(Promise.all(promises).then(async e => {
|
||||
if (weaponStats.length > 0) {
|
||||
await db.insertInto('ToneAPI_v3.weapon_stats_in_match')
|
||||
.values(weaponStats)
|
||||
.execute()
|
||||
}
|
||||
}))
|
||||
|
||||
playerPromises.push(Promise.all(promises).then(async e => {
|
||||
if (titanStats.length > 0) {
|
||||
await db.insertInto('ToneAPI_v3.titan_stats_in_match')
|
||||
.values(titanStats)
|
||||
.execute()
|
||||
}
|
||||
}))
|
||||
|
||||
playerPromises.push(db.insertInto('ToneAPI_v3.player_stats_in_match')
|
||||
.values({
|
||||
distance_air: playerData.stats.distance.air,
|
||||
distance_ground: playerData.stats.distance.ground,
|
||||
distance_wall: playerData.stats.distance.wall,
|
||||
time_air: playerData.stats.time.air,
|
||||
time_ground: playerData.stats.time.ground,
|
||||
time_wall: playerData.stats.time.wall,
|
||||
match_id,
|
||||
player_id: NumPlayerId
|
||||
})
|
||||
.execute())
|
||||
}
|
||||
await Promise.all(playerPromises)
|
||||
await db.updateTable('ToneAPI_v3.match')
|
||||
.set({ ongoing: false })
|
||||
.where('match_id', '=', match_id)
|
||||
.execute()
|
||||
res.sendStatus(201)
|
||||
})()
|
||||
}
|
||||
)
|
||||
|
||||
export default router
|
||||
@@ -1,34 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { Router, type Response } from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { validateErrors } from '../common'
|
||||
import { validateBody, type RequestBody } from '../types'
|
||||
import typia from 'typia'
|
||||
import { checkUpdateOrCreatePlayer } from '../utils'
|
||||
|
||||
const router = Router()
|
||||
|
||||
interface PlayerConnect {
|
||||
username: string
|
||||
match_id: number
|
||||
}
|
||||
|
||||
router.put(
|
||||
'/:playerId/connect',
|
||||
param('playerId').exists().withMessage('Missing Player ID').bail().isNumeric().withMessage('Player ID is not numeric'),
|
||||
validateErrors,
|
||||
validateBody(typia.createValidate<PlayerConnect>()),
|
||||
(req: RequestBody<PlayerConnect>, res: Response) => {
|
||||
void (async () => {
|
||||
console.log('player connected')
|
||||
const { username } = req.body
|
||||
await checkUpdateOrCreatePlayer({
|
||||
id: req.params.playerId,
|
||||
name: username
|
||||
})
|
||||
res.status(200).send()
|
||||
})()
|
||||
}
|
||||
)
|
||||
|
||||
export default router
|
||||
@@ -1,83 +0,0 @@
|
||||
import { Router } from 'express'
|
||||
import { header } from 'express-validator'
|
||||
|
||||
import { /* createKillRecord, */ checkServerToken } from '../db'
|
||||
import { validateErrors } from '../common'
|
||||
|
||||
import match from './match'
|
||||
import kill from './kill'
|
||||
import player from './player'
|
||||
|
||||
const router = Router()
|
||||
|
||||
// auth middleware
|
||||
router.post(
|
||||
'/*',
|
||||
header('authorization')
|
||||
.exists({ checkFalsy: true })
|
||||
.withMessage('Missing Authorization Header')
|
||||
.bail()
|
||||
.custom((e) => e.split(' ')[0].toLowerCase() === 'bearer')
|
||||
.withMessage('Authorization Token is not Bearer'),
|
||||
validateErrors,
|
||||
(req, res, next) => {
|
||||
void (async () => {
|
||||
if (!req.headers.authorization) {
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
const query = await checkServerToken(
|
||||
req.headers.authorization.split(' ')[1]
|
||||
)
|
||||
if (!query?.host_id) {
|
||||
console.error(
|
||||
`incorrect token : ${req.headers.authorization.split(' ')[1]
|
||||
} with IP ${req.headers['x-forwarded-for']?.toString() ??
|
||||
req.socket.remoteAddress?.toString() ??
|
||||
''
|
||||
}`
|
||||
)
|
||||
return res.status(401).send({
|
||||
errors: [
|
||||
{
|
||||
msg: 'Incorrect Token',
|
||||
param: 'authorization',
|
||||
location: 'headers'
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
res.locals.host_id = query.host_id
|
||||
next()
|
||||
})()
|
||||
}
|
||||
)
|
||||
|
||||
// Route to check auth
|
||||
router.post('/', (req, res) => {
|
||||
res.sendStatus(200)
|
||||
})
|
||||
|
||||
// const serversCount: Record<string, number> = {}
|
||||
// const serversTimeout: Record<string, NodeJS.Timeout> = {}
|
||||
|
||||
// same rate limiting code as register. max 10 kills per server every 1 sec. should be enough.
|
||||
/* router.post('/kill', (req, res, next) => {
|
||||
let host = Number(req.query.serverId)
|
||||
if (serversCount[serverId] > 2) {
|
||||
return res.status(429).json({
|
||||
error: 'too many requests. Are players really making that much kills ?'
|
||||
})
|
||||
}
|
||||
clearTimeout(serversTimeout[serverId])
|
||||
serversCount[serverId] =
|
||||
serversCount[serverId] ?? (serversCount[serverId] + 1) | 1
|
||||
serversTimeout[serverId] = setTimeout(() => {
|
||||
serversCount[serverId] = 0
|
||||
}, 1000)
|
||||
next()
|
||||
}) */
|
||||
|
||||
router.use('/kill', kill)
|
||||
router.use('/match', match)
|
||||
router.use('/player', player)
|
||||
export default router
|
||||
@@ -1,85 +0,0 @@
|
||||
import { type RequestHandler, type Request } from 'express'
|
||||
import type typia from 'typia'
|
||||
|
||||
interface WeaponKillData {
|
||||
id: string
|
||||
mods: number
|
||||
}
|
||||
|
||||
export interface LoadoutKillData {
|
||||
ordnance: WeaponKillData | undefined
|
||||
secondary: WeaponKillData | undefined
|
||||
primary: WeaponKillData | undefined
|
||||
tactical: WeaponKillData | undefined
|
||||
anti_titan: WeaponKillData | undefined
|
||||
passive1: string | undefined
|
||||
passive2: string | undefined
|
||||
titan: string | undefined
|
||||
}
|
||||
|
||||
interface PlayerKillData {
|
||||
velocity: number
|
||||
loadout: LoadoutKillData
|
||||
current_weapon: WeaponKillData
|
||||
state: string
|
||||
id: string
|
||||
cloaked: boolean
|
||||
}
|
||||
|
||||
export interface MatchData {
|
||||
server_name: string
|
||||
game_map: string
|
||||
gamemode: string
|
||||
air_accel: boolean
|
||||
}
|
||||
|
||||
export interface KillData {
|
||||
game_time: number
|
||||
player_count: number
|
||||
match_id: number
|
||||
victim: PlayerKillData
|
||||
attacker: PlayerKillData
|
||||
distance: number
|
||||
cause_of_death: string
|
||||
}
|
||||
|
||||
export type MatchCloseData = Record<string, MatchClosePlayerData>
|
||||
export interface MatchClosePlayerData {
|
||||
weapons: Record<string, MatchCloseWeaponData>
|
||||
titans: Record<string, MatchCloseWeaponData>
|
||||
stats: {
|
||||
distance: {
|
||||
ground: number
|
||||
wall: number
|
||||
air: number
|
||||
}
|
||||
time: {
|
||||
ground: number
|
||||
wall: number
|
||||
air: number
|
||||
}
|
||||
}
|
||||
}
|
||||
export interface MatchCloseWeaponData {
|
||||
shotsFired: number
|
||||
shotsHit: number
|
||||
shotsCrit: number
|
||||
shotsHeadshot: number
|
||||
shotsRichochet: number
|
||||
playtime: number
|
||||
}
|
||||
|
||||
export type RequestBody<T> = Request<any, any, T>
|
||||
|
||||
export const validateBody =
|
||||
<T>(checker: (input: T) => typia.IValidation<T>): RequestHandler =>
|
||||
(req, res, next) => {
|
||||
const result: typia.IValidation<T> = checker(req.body)
|
||||
if (!result.success) {
|
||||
res.status(400).send({ errors: result.errors })
|
||||
console.log(req.body)
|
||||
console.error(result.errors)
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
}
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
import { type Transaction } from 'kysely'
|
||||
import db from './db'
|
||||
import type Database from './db/model'
|
||||
import { type LoadoutKillData } from './types'
|
||||
|
||||
export async function createOrRunInTransaction (callback: (trx: Transaction<Database>) => Promise<void>, trx?: Transaction<Database>) {
|
||||
if (trx) {
|
||||
await callback(trx)
|
||||
} else {
|
||||
await db
|
||||
.transaction()
|
||||
.setIsolationLevel('serializable')
|
||||
.execute(callback)
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkUpdateOrCreatePlayer (data: {
|
||||
id: number
|
||||
name: string
|
||||
}) {
|
||||
await db
|
||||
.transaction()
|
||||
.setIsolationLevel('serializable')
|
||||
.execute(async (trx) => {
|
||||
const player = await trx
|
||||
.selectFrom('ToneAPI_v3.player')
|
||||
.select(['player_name'])
|
||||
.where('player_id', '=', data.id)
|
||||
.executeTakeFirst()
|
||||
if (!player) {
|
||||
await trx
|
||||
.insertInto('ToneAPI_v3.player')
|
||||
.values({
|
||||
player_id: data.id,
|
||||
player_name: data.name
|
||||
})
|
||||
.execute()
|
||||
} else if (player.player_name !== data.name) {
|
||||
await trx
|
||||
.updateTable('ToneAPI_v3.player')
|
||||
.set({ player_name: data.name })
|
||||
.where('ToneAPI_v3.player.player_id', '=', data.id)
|
||||
.execute()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function checkOrCreateWeapon (weaponID: string, trx?: Transaction<Database>) {
|
||||
const runner = async (trx: Transaction<Database>) => {
|
||||
const weapon = await trx
|
||||
.selectFrom('ToneAPI_v3.weapon')
|
||||
.select('ToneAPI_v3.weapon.weapon_id')
|
||||
.where('weapon_id', '=', weaponID)
|
||||
.executeTakeFirst()
|
||||
if (!weapon) {
|
||||
await trx
|
||||
.insertInto('ToneAPI_v3.weapon')
|
||||
.values({
|
||||
weapon_id: weaponID
|
||||
})
|
||||
.execute()
|
||||
}
|
||||
}
|
||||
|
||||
await createOrRunInTransaction(runner, trx)
|
||||
}
|
||||
|
||||
export async function checkOrCreateWeaponMods (weapon: {
|
||||
id: string
|
||||
mods: number
|
||||
}, trx?: Transaction<Database>) {
|
||||
const runner = async (trx: Transaction<Database>) => {
|
||||
await checkOrCreateWeapon(weapon.id, trx)
|
||||
const weaponMods = await trx
|
||||
.selectFrom('ToneAPI_v3.mods_on_weapon')
|
||||
.select('ToneAPI_v3.mods_on_weapon.mod_id')
|
||||
.where('mod_id', '=', weapon.mods)
|
||||
.where('weapon_id', '=', weapon.id)
|
||||
.executeTakeFirst()
|
||||
if (!weaponMods) {
|
||||
await trx
|
||||
.insertInto('ToneAPI_v3.mods_on_weapon')
|
||||
.values({
|
||||
mod_id: weapon.mods,
|
||||
weapon_id: weapon.id,
|
||||
autogenerated: true
|
||||
})
|
||||
.execute()
|
||||
}
|
||||
}
|
||||
|
||||
await createOrRunInTransaction(runner, trx)
|
||||
}
|
||||
|
||||
export async function checkOrCreateTitan (titanID: string | null, trx?: Transaction<Database>) {
|
||||
if (titanID == null) {
|
||||
return
|
||||
}
|
||||
const runner = async (trx: Transaction<Database>) => {
|
||||
const titan = await trx
|
||||
.selectFrom('ToneAPI_v3.titan_chassis')
|
||||
.select('ToneAPI_v3.titan_chassis.titan_id')
|
||||
.where('titan_id', '=', titanID)
|
||||
.executeTakeFirst()
|
||||
if (!titan) {
|
||||
await trx
|
||||
.insertInto('ToneAPI_v3.titan_chassis')
|
||||
.values({
|
||||
titan_id: titanID
|
||||
})
|
||||
.execute()
|
||||
}
|
||||
}
|
||||
|
||||
await createOrRunInTransaction(runner, trx)
|
||||
}
|
||||
|
||||
export async function checkOrCreateLoadout (loadoutData: LoadoutKillData) {
|
||||
const loadout = await db
|
||||
.transaction()
|
||||
.setIsolationLevel('serializable')
|
||||
.execute(async (trx) => {
|
||||
const loadout = await trx
|
||||
.selectFrom('ToneAPI_v3.loadout')
|
||||
.select('ToneAPI_v3.loadout.loadout_id')
|
||||
.where('primary_weapon', loadoutData.primary?.id !== undefined ? '=' : 'is', loadoutData.primary?.id ?? null)
|
||||
.where('primary_mod_id', loadoutData.primary?.mods !== undefined ? '=' : 'is', loadoutData.primary?.mods ?? null)
|
||||
.where('secondary_weapon', loadoutData.secondary?.id !== undefined ? '=' : 'is', loadoutData.secondary?.id ?? null)
|
||||
.where('secondary_mod_id', loadoutData.secondary?.mods !== undefined ? '=' : 'is', loadoutData.secondary?.mods ?? null)
|
||||
.where('anti_titan_weapon', loadoutData.anti_titan?.id !== undefined ? '=' : 'is', loadoutData.anti_titan?.id ?? null)
|
||||
.where('anti_titan_mod_id', loadoutData.anti_titan?.mods !== undefined ? '=' : 'is', loadoutData.anti_titan?.mods ?? null)
|
||||
.where('ordnance', loadoutData.ordnance?.id !== undefined ? '=' : 'is', loadoutData.ordnance?.id ?? null)
|
||||
.where('tactical', loadoutData.tactical?.id !== undefined ? '=' : 'is', loadoutData.tactical?.id ?? null)
|
||||
.where('pilot_passive_1', loadoutData.passive1 !== undefined ? '=' : 'is', loadoutData.passive1 ?? null)
|
||||
.where('pilot_passive_2', loadoutData.passive2 !== undefined ? '=' : 'is', loadoutData.passive2 ?? null)
|
||||
.where('titan_id', loadoutData.titan !== undefined ? '=' : 'is', loadoutData.titan ?? null)
|
||||
.executeTakeFirst()
|
||||
if (!loadout) {
|
||||
if (loadoutData.primary !== undefined) {
|
||||
await checkOrCreateWeaponMods(loadoutData.primary, trx)
|
||||
}
|
||||
if (loadoutData.secondary !== undefined) {
|
||||
await checkOrCreateWeaponMods(loadoutData.secondary, trx)
|
||||
}
|
||||
if (loadoutData.anti_titan !== undefined) {
|
||||
await checkOrCreateWeaponMods(loadoutData.anti_titan, trx)
|
||||
}
|
||||
if (loadoutData.ordnance !== undefined) {
|
||||
await checkOrCreateWeapon(loadoutData.ordnance.id, trx)
|
||||
}
|
||||
if (loadoutData.titan !== undefined) {
|
||||
await checkOrCreateTitan(loadoutData.titan, trx)
|
||||
}
|
||||
const result = await trx
|
||||
.insertInto('ToneAPI_v3.loadout')
|
||||
.values({
|
||||
primary_weapon: loadoutData.primary?.id ?? null,
|
||||
primary_mod_id: loadoutData.primary?.mods ?? null,
|
||||
secondary_weapon: loadoutData.secondary?.id ?? null,
|
||||
secondary_mod_id: loadoutData.secondary?.mods ?? null,
|
||||
anti_titan_weapon: loadoutData.anti_titan?.id ?? null,
|
||||
anti_titan_mod_id: loadoutData.anti_titan?.mods ?? null,
|
||||
ordnance: loadoutData.ordnance?.id ?? null,
|
||||
tactical: loadoutData.tactical?.id ?? null,
|
||||
pilot_passive_1: loadoutData.passive1,
|
||||
pilot_passive_2: loadoutData.passive2,
|
||||
titan_id: loadoutData.titan
|
||||
})
|
||||
.returning('ToneAPI_v3.loadout.loadout_id')
|
||||
.executeTakeFirstOrThrow()
|
||||
return result
|
||||
}
|
||||
return loadout
|
||||
})
|
||||
if (!loadout) {
|
||||
throw Error('loadout is undefined')
|
||||
}
|
||||
return loadout.loadout_id
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import * as dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
import { Client } from 'pg'
|
||||
import { WebSocket, WebSocketServer } from 'ws'
|
||||
import { KillTable } from './db/model'
|
||||
|
||||
const port = 3002
|
||||
const wss = new WebSocketServer({ port })
|
||||
|
||||
export const pgClient = new Client({
|
||||
host: process.env.POSTGRES_HOST,
|
||||
database: process.env.POSTGRES_DATABASE,
|
||||
user: process.env.POSTGRES_USER,
|
||||
password: process.env.POSTGRES_PASSWORD
|
||||
})
|
||||
|
||||
wss.on('connection', function connection(ws: WebSocket & { isAlive: boolean }, req) {
|
||||
const ip =
|
||||
req?.headers['x-forwarded-for']?.toString().split(',')[0].trim() ||
|
||||
req.socket.remoteAddress
|
||||
console.log(
|
||||
new Date().toLocaleString() + ',' + ip + ',connect,' + wss.clients.size
|
||||
)
|
||||
ws.onclose = function () {
|
||||
const ip =
|
||||
req?.headers['x-forwarded-for']?.toString().split(',')[0].trim() ||
|
||||
req.socket.remoteAddress
|
||||
console.log(
|
||||
new Date().toLocaleString() + ',' + ip + ',close,' + wss.clients.size
|
||||
)
|
||||
}
|
||||
ws.onmessage = function (msg) {
|
||||
if (msg.data === "pong") {
|
||||
return ws.isAlive = true
|
||||
}
|
||||
if (msg.data === "ping") {
|
||||
return ws.send("pong")
|
||||
}
|
||||
}
|
||||
ws.send("ping");
|
||||
})
|
||||
|
||||
const interval = setInterval(function ping() {
|
||||
wss.clients.forEach(function each(ws) {
|
||||
if ((ws as WebSocket & { isAlive: boolean }).isAlive === false) return ws.terminate();
|
||||
|
||||
(ws as WebSocket & { isAlive: boolean }).isAlive = false;
|
||||
ws.send("ping");
|
||||
});
|
||||
}, 30000);
|
||||
|
||||
wss.on('close', function close() {
|
||||
clearInterval(interval);
|
||||
});
|
||||
|
||||
export default new Promise(async (resolve, reject) => {
|
||||
await pgClient.connect()
|
||||
await pgClient.query('LISTEN new_kill')
|
||||
|
||||
pgClient.on('notification', (data) => {
|
||||
if (!data.payload) return
|
||||
const payload = JSON.parse(data.payload) as KillTable
|
||||
wss.clients.forEach(function (client) {
|
||||
if (client.readyState === client.OPEN) {
|
||||
client.send(
|
||||
JSON.stringify({
|
||||
match_id: payload.match_id,
|
||||
attacker_id: payload.attacker_id,
|
||||
attacker_name: payload.attacker_name,
|
||||
cause_of_death: payload.cause_of_death,
|
||||
victim_id: payload.victim_id,
|
||||
victim_name: payload.victim_name,
|
||||
attacker_current_weapon: payload.attacker_current_weapon,
|
||||
victim_current_weapon: payload.victim_current_weapon,
|
||||
distance: payload.distance,
|
||||
game_mode: payload.game_mode,
|
||||
servername: payload.servername,
|
||||
map: payload.map,
|
||||
host: payload.host
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
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;
|
||||
})
|
||||
|
||||
describe('client', () => {
|
||||
test('server list', async () => {
|
||||
const request = await fetch("http://127.0.0.1:3000/servers")
|
||||
const data = await request.json()
|
||||
const first = Object.entries(data)[0]
|
||||
expect(first[1]).toHaveProperty('max_distance')
|
||||
expect(first[1]).toHaveProperty('total_distance')
|
||||
expect(first[1]).toHaveProperty('kills')
|
||||
})
|
||||
|
||||
test('player list', async () => {
|
||||
const request = await fetch("http://127.0.0.1:3000/players")
|
||||
const data = await request.json()
|
||||
const first = Object.entries(data)[0]
|
||||
expect(first[1]).toHaveProperty('max_distance')
|
||||
expect(first[1]).toHaveProperty('total_distance')
|
||||
expect(first[1]).toHaveProperty('kills')
|
||||
})
|
||||
|
||||
test('player list with weapon filter', async () => {
|
||||
const request = await fetch("http://127.0.0.1:3000/players?weapons=sniper")
|
||||
const data = await request.json()
|
||||
const first = Object.entries(data)[0]
|
||||
expect(first[1]).toHaveProperty('max_distance')
|
||||
expect(first[1]).toHaveProperty('total_distance')
|
||||
expect(first[1]).toHaveProperty('kills')
|
||||
})
|
||||
|
||||
test('weapon list', async () => {
|
||||
const request = await fetch("http://127.0.0.1:3000/weapons")
|
||||
const data = await request.json()
|
||||
const first = Object.entries(data)[0]
|
||||
expect(first[1]).toHaveProperty('max_distance')
|
||||
expect(first[1]).toHaveProperty('total_distance')
|
||||
expect(first[1]).toHaveProperty('kills')
|
||||
})
|
||||
|
||||
test('weapon list with player filter', async () => {
|
||||
const request = await fetch("http://127.0.0.1:3000/weapons?players=1005930844007")
|
||||
const data = await request.json()
|
||||
const first = Object.entries(data)[0]
|
||||
expect(first[1]).toHaveProperty('max_distance')
|
||||
expect(first[1]).toHaveProperty('total_distance')
|
||||
expect(first[1]).toHaveProperty('kills')
|
||||
})
|
||||
})
|
||||
|
||||
afterAll((done) => {
|
||||
listenServer.close(async () => {
|
||||
await pgClient.end()
|
||||
await db.destroy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,161 @@
|
||||
import { afterAll, beforeAll, describe, expect, jest, test } from '@jest/globals'
|
||||
import clientMain from '../src/clientMain'
|
||||
import serverMain from '../src/serverMain'
|
||||
import { pgClient } from '../src/process/onKill'
|
||||
import * as dotenv from 'dotenv'
|
||||
import db from '../src/db/db'
|
||||
dotenv.config()
|
||||
|
||||
let listenClient
|
||||
let listenServer
|
||||
|
||||
const data = {
|
||||
servername: 'testserver',
|
||||
attacker_weapon_1_mods: 0,
|
||||
victim_id: '0',
|
||||
victim_name: 'TestVictim',
|
||||
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: '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',
|
||||
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: 'tdm',
|
||||
map: 'thaw',
|
||||
attacker_weapon_2: 'autopistol',
|
||||
victim_weapon_1: 'null',
|
||||
victim_weapon_2: 'autopistol',
|
||||
victim_weapon_1_mods: 0,
|
||||
victim_weapon_3: 'defender',
|
||||
attacker_name: 'TestAttacker',
|
||||
victim_titan: 'null',
|
||||
attacker_titan: 'null'
|
||||
}
|
||||
|
||||
function waitFor(time: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, time)
|
||||
})
|
||||
}
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
beforeAll(async () => {
|
||||
listenClient = await clientMain;
|
||||
listenServer = await serverMain;
|
||||
const yea = waitFor(1000)
|
||||
const response = await fetch(`http://127.0.0.1:3001/kill`, {
|
||||
method: "POST", // *GET, POST, PUT, DELETE, etc.
|
||||
credentials: "same-origin", // include, *same-origin, omit
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Authorization': `Bearer ${Buffer.from('' + process.env.SERVERAUTH_TOKEN).toString('base64')}`
|
||||
},
|
||||
body: JSON.stringify(data), // body data type must match "Content-Type" header
|
||||
});
|
||||
expect(response.status).toBe(201)
|
||||
await yea
|
||||
})
|
||||
|
||||
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()
|
||||
const player = data["1"]
|
||||
expect(player).toHaveProperty('max_distance')
|
||||
expect(player).toHaveProperty('total_distance')
|
||||
expect(player).toHaveProperty('kills')
|
||||
playerKills = player.kills
|
||||
playerDeaths = player.deaths
|
||||
})
|
||||
|
||||
test('fetch weapon', async () => {
|
||||
const request = await fetch("http://127.0.0.1:3000/weapons")
|
||||
const data = await request.json()
|
||||
const weapon = data.smr
|
||||
expect(weapon).toHaveProperty('max_distance')
|
||||
expect(weapon).toHaveProperty('total_distance')
|
||||
expect(weapon).toHaveProperty('kills')
|
||||
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 response = await fetch(`http://127.0.0.1:3001/kill`, {
|
||||
method: "POST", // *GET, POST, PUT, DELETE, etc.
|
||||
credentials: "same-origin", // include, *same-origin, omit
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Authorization': `Bearer ${Buffer.from('' + process.env.SERVERAUTH_TOKEN).toString('base64')}`
|
||||
},
|
||||
body: JSON.stringify(data), // body data type must match "Content-Type" header
|
||||
});
|
||||
expect(response.status).toBe(201)
|
||||
const yea = waitFor(1000)
|
||||
await yea
|
||||
})
|
||||
|
||||
test('check player update', async () => {
|
||||
|
||||
const request = await fetch("http://127.0.0.1:3000/players")
|
||||
const data = await request.json()
|
||||
const player = data["1"]
|
||||
expect(player.kills).toBe(playerKills + 1)
|
||||
})
|
||||
|
||||
test('check weapon update', async () => {
|
||||
const request = await fetch("http://127.0.0.1:3000/weapons")
|
||||
const data = await request.json()
|
||||
const weapon = data.smr
|
||||
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) => {
|
||||
listenClient.close(() => {
|
||||
listenServer.close(async () => {
|
||||
await pgClient.end()
|
||||
await db.destroy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
+139
-217
@@ -1,258 +1,180 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from '@jest/globals'
|
||||
import serverMain from '../src/serverMain'
|
||||
import db, { dbReady } from '../src/db'
|
||||
import db from "../src/db/db"
|
||||
import * as dotenv from 'dotenv'
|
||||
import { type MatchCloseData, type KillData } from '../src/types'
|
||||
dotenv.config()
|
||||
|
||||
let listenServer
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${Buffer.from(process.env.SERVERAUTH_TOKEN + '').toString('base64')}`
|
||||
}
|
||||
const testMatch = { air_accel: false, server_name: 'servertest' + Math.floor(Math.random() * 100).toString(), game_map: 'testMap', gamemode: 'test' }
|
||||
const testKill: KillData = {
|
||||
game_time: 22.61666870117186,
|
||||
player_count: 1,
|
||||
match_id: 1,
|
||||
victim: {
|
||||
velocity: 0.0,
|
||||
name: 'Legonzaur',
|
||||
loadout: {
|
||||
titan: 'testTitan',
|
||||
passive1: 'testPassive1',
|
||||
passive2: 'testPassive2',
|
||||
ordnance: {
|
||||
id: 'mp_weapon_satchel',
|
||||
mods: 0
|
||||
},
|
||||
secondary: {
|
||||
id: 'mp_weapon_wingman',
|
||||
mods: 140
|
||||
},
|
||||
primary: {
|
||||
id: 'mp_weapon_sniper',
|
||||
mods: 1168
|
||||
},
|
||||
tactical: {
|
||||
id: 'mp_ability_grapple',
|
||||
mods: 8
|
||||
},
|
||||
anti_titan: {
|
||||
id: 'mp_weapon_defender',
|
||||
mods: 134
|
||||
}
|
||||
},
|
||||
current_weapon: {
|
||||
id: 'mp_weapon_sniper',
|
||||
mods: 1168
|
||||
},
|
||||
state: 'OnGround',
|
||||
id: '1005930844007',
|
||||
cloaked: false
|
||||
},
|
||||
attacker: {
|
||||
velocity: 0.0,
|
||||
name: 'Legonzaur',
|
||||
loadout: {
|
||||
titan: 'testTitan',
|
||||
passive1: 'testPassive1',
|
||||
passive2: 'testPassive2',
|
||||
ordnance: {
|
||||
id: 'mp_weapon_satchel',
|
||||
mods: 0
|
||||
},
|
||||
secondary: {
|
||||
id: 'mp_weapon_wingman',
|
||||
mods: 140
|
||||
},
|
||||
primary: {
|
||||
id: 'mp_weapon_sniper',
|
||||
mods: 1168
|
||||
},
|
||||
tactical: {
|
||||
id: 'mp_ability_grapple',
|
||||
mods: 8
|
||||
},
|
||||
anti_titan: {
|
||||
id: 'mp_weapon_defender',
|
||||
mods: 134
|
||||
}
|
||||
},
|
||||
current_weapon: {
|
||||
id: 'mp_weapon_sniper',
|
||||
mods: 1168
|
||||
},
|
||||
state: 'OnGround',
|
||||
id: '1005930844007',
|
||||
cloaked: false
|
||||
},
|
||||
distance: 0.0,
|
||||
cause_of_death: 'mp_weapon_satchel'
|
||||
}
|
||||
|
||||
const testMatchStats: MatchCloseData = {
|
||||
1005930844007: {
|
||||
stats: {
|
||||
distance: {
|
||||
air: 122.2,
|
||||
ground: 50.4,
|
||||
wall: 20.1
|
||||
},
|
||||
time: {
|
||||
air: 1220.2,
|
||||
ground: 500.4,
|
||||
wall: 200.1
|
||||
}
|
||||
},
|
||||
weapons: {
|
||||
mp_testweapon: {
|
||||
playtime: 1569.1,
|
||||
shotsCrit: 15,
|
||||
shotsFired: 5,
|
||||
shotsHeadshot: 9,
|
||||
shotsHit: 4,
|
||||
shotsRichochet: 0
|
||||
}
|
||||
},
|
||||
titans: {
|
||||
testTitan: {
|
||||
playtime: 1569.1,
|
||||
shotsCrit: 15,
|
||||
shotsFired: 5,
|
||||
shotsHeadshot: 9,
|
||||
shotsHit: 4,
|
||||
shotsRichochet: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
listenServer = await serverMain
|
||||
await dbReady()
|
||||
listenServer = await serverMain;
|
||||
})
|
||||
|
||||
describe('auth', () => {
|
||||
describe('server', () => {
|
||||
|
||||
test('bad auth prefetch', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3001/', {
|
||||
method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
credentials: 'same-origin', // include, *same-origin, omit
|
||||
const response = await fetch(`http://127.0.0.1:3001/`, {
|
||||
method: "POST", // *GET, POST, PUT, DELETE, etc.
|
||||
credentials: "same-origin", // include, *same-origin, omit
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearere ${Buffer.from('' + process.env.SERVERAUTH_TOKEN).toString('base64')}`
|
||||
"Content-Type": "application/json",
|
||||
'Authorization': `Bearere ${Buffer.from('' + process.env.SERVERAUTH_TOKEN).toString('base64')}`
|
||||
}
|
||||
})
|
||||
});
|
||||
expect(response.status).toBe(400)
|
||||
|
||||
const response2 = await fetch('http://127.0.0.1:3001/', {
|
||||
method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
credentials: 'same-origin', // include, *same-origin, omit
|
||||
const response2 = await fetch(`http://127.0.0.1:3001/`, {
|
||||
method: "POST", // *GET, POST, PUT, DELETE, etc.
|
||||
credentials: "same-origin", // include, *same-origin, omit
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${Buffer.from('badtoken').toString('base64')}`
|
||||
"Content-Type": "application/json",
|
||||
'Authorization': `Bearer ${Buffer.from('badtoken').toString('base64')}`
|
||||
}
|
||||
})
|
||||
expect(response2.status).toBe(401)
|
||||
});
|
||||
expect(response2.status).toBe(403)
|
||||
})
|
||||
|
||||
test('bad kill token', async () => {
|
||||
const response2 = await fetch('http://127.0.0.1:3001/kill', {
|
||||
method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
credentials: 'same-origin', // include, *same-origin, omit
|
||||
const response2 = await fetch(`http://127.0.0.1:3001/kill`, {
|
||||
method: "POST", // *GET, POST, PUT, DELETE, etc.
|
||||
credentials: "same-origin", // include, *same-origin, omit
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${Buffer.from('badtoken').toString('base64')}`
|
||||
"Content-Type": "application/json",
|
||||
'Authorization': `Bearer ${Buffer.from('badtoken').toString('base64')}`
|
||||
}
|
||||
})
|
||||
expect(response2.status).toBe(401)
|
||||
});
|
||||
expect(response2.status).toBe(403)
|
||||
})
|
||||
|
||||
|
||||
test('good auth prefetch', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3001/', {
|
||||
method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
credentials: 'same-origin', // include, *same-origin, omit
|
||||
headers
|
||||
})
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
})
|
||||
|
||||
describe('stats', () => {
|
||||
let matchId: string
|
||||
test('register a match', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3001/match', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(testMatch)
|
||||
})
|
||||
const json = await response.json()
|
||||
matchId = json.match
|
||||
testKill.match_id = Number(matchId)
|
||||
expect(json).toHaveProperty('match')
|
||||
expect(json.match).not.toBeNaN()
|
||||
expect(response.status).toBe(201)
|
||||
})
|
||||
|
||||
test('register a player', async () => {
|
||||
const response = await fetch(`http://127.0.0.1:3001/player/${testKill.attacker.id}/connect`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ username: 'Legonzaur', match_id: testKill.match_id })
|
||||
})
|
||||
console.log(await response.text())
|
||||
const response = await fetch(`http://127.0.0.1:3001/`, {
|
||||
method: "POST", // *GET, POST, PUT, DELETE, etc.
|
||||
credentials: "same-origin", // include, *same-origin, omit
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Authorization': `Bearer ${Buffer.from('' + process.env.SERVERAUTH_TOKEN).toString('base64')}`
|
||||
}
|
||||
});
|
||||
expect(response.status).toBe(200)
|
||||
})
|
||||
|
||||
test('register a kill', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3001/kill', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(testKill)
|
||||
})
|
||||
const data = {
|
||||
servername: 'testserver',
|
||||
attacker_weapon_1_mods: 0,
|
||||
victim_id: '0',
|
||||
victim_name: 'TestVictim',
|
||||
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: 'offhand_weapon_test',
|
||||
attacker_offhand_weapon_1_mods: 0,
|
||||
attacker_weapon_3_mods: 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',
|
||||
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: 'tdm',
|
||||
map: 'thaw',
|
||||
attacker_weapon_2: 'autopistol',
|
||||
victim_weapon_1: 'smr',
|
||||
victim_weapon_2: 'autopistol',
|
||||
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.
|
||||
credentials: "same-origin", // include, *same-origin, omit
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Authorization': `Bearer ${Buffer.from(process.env.SERVERAUTH_TOKEN + '').toString('base64')}`
|
||||
},
|
||||
body: JSON.stringify(data), // body data type must match "Content-Type" header
|
||||
});
|
||||
console.log(await response.text())
|
||||
expect(response.status).toBe(201)
|
||||
})
|
||||
|
||||
// test('register a kill with missing data', async () => {
|
||||
// const data = testKill
|
||||
// const response = await fetch('http://127.0.0.1:3001/kill', {
|
||||
// method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
// credentials: 'same-origin', // include, *same-origin, omit
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// Authorization: `Bearer ${Buffer.from(process.env.SERVERAUTH_TOKEN + '').toString('base64')}`
|
||||
// },
|
||||
// body: JSON.stringify(data)
|
||||
// })
|
||||
// expect(response.status).toBe(400)
|
||||
// })
|
||||
|
||||
test('close a match', async () => {
|
||||
const response = await fetch(`http://127.0.0.1:3001/match/${matchId}/close`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(testMatchStats)
|
||||
})
|
||||
test('register a kill with missing data', async () => {
|
||||
const data = {
|
||||
servername: 'testserver',
|
||||
attacker_weapon_1_mods: 0,
|
||||
victim_id: '0',
|
||||
victim_name: 'TestVictim',
|
||||
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: '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',
|
||||
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: 'tdm',
|
||||
map: 'thaw',
|
||||
attacker_weapon_2: 'autopistol',
|
||||
victim_weapon_1: 'null',
|
||||
victim_weapon_2: 'autopistol',
|
||||
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.
|
||||
credentials: "same-origin", // include, *same-origin, omit
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Authorization': `Bearer ${Buffer.from(process.env.SERVERAUTH_TOKEN + '').toString('base64')}`
|
||||
},
|
||||
body: JSON.stringify(data), // body data type must match "Content-Type" header
|
||||
});
|
||||
expect(response.status).toBe(201)
|
||||
})
|
||||
|
||||
test('register a kill after a match is closed', async () => {
|
||||
const response = await fetch('http://127.0.0.1:3001/kill', {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(testKill)
|
||||
})
|
||||
expect(response.status).toBe(409)
|
||||
})
|
||||
})
|
||||
|
||||
afterAll((done) => {
|
||||
// db.deleteFrom('kill').where('attacker_id', '=', 0n).orWhere('attacker_id', '=', 1n).execute().then(() => {
|
||||
listenServer.close(async () => {
|
||||
await db.destroy()
|
||||
done()
|
||||
db.deleteFrom('kill').where('attacker_id', '=', '0').orWhere('attacker_id', '=', '1').execute().then(() => {
|
||||
listenServer.close(async () => {
|
||||
await db.destroy()
|
||||
done()
|
||||
})
|
||||
})
|
||||
// })
|
||||
})
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs" /* Specify what module code is generated. */,
|
||||
// "rootDir": "./" /* Specify the root folder within your source files. */,
|
||||
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"sourceMap": true /* Create source map files for emitted JavaScript files. */,
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./out/" /* Specify an output folder for all emitted files. */,
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"compileOnSave": true,
|
||||
"exclude": ["jest.config.ts"]
|
||||
}
|
||||
+1
-1
@@ -101,5 +101,5 @@
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
},
|
||||
"compileOnSave": true,
|
||||
"exclude": ["jest.config.ts", "./tests", "./src/templates"]
|
||||
"exclude": ["jest.config.ts", "./tests"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user