3 Commits
Author SHA1 Message Date
Okudai 562ccf0060 Update index.html (#56) 2023-10-31 18:29:00 +01:00
Okudai 17cb562d82 Update v2.yml
Update v2.yml
2023-10-30 21:50:46 +01:00
Okudai a145baab8b Update v2.yml
Change url in documentation to the proper one
2023-10-30 21:49:46 +01:00
48 changed files with 5862 additions and 6055 deletions
-5
View File
@@ -3,8 +3,3 @@ node_modules
.env.example
npm-debug.log
out
src/generated
coverage
.github
.vscode
.stoplight
-2
View File
@@ -1,2 +0,0 @@
out
.eslintrc.js
+15 -29
View File
@@ -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'
}
}
-1
View File
@@ -103,4 +103,3 @@ dist
# TernJS port file
.tern-port
out/
src/generated
-3
View File
@@ -1,3 +0,0 @@
[submodule "src/db"]
path = src/db
url = https://github.com/ToneAPI/backend-model
+20 -2
View File
@@ -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"]
}
]
}
+1 -2
View File
@@ -6,6 +6,5 @@
"eslint.format.enable": true,
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"typescript.tsdk": "node_modules/typescript/lib"
}
}
-25
View File
@@ -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"
}
]
}
-21
View File
@@ -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
Submodule docs deleted from 4c24c6525f
+465
View File
File diff suppressed because one or more lines are too long
+592
View File
@@ -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
View File
@@ -0,0 +1,453 @@
openapi: 3.0.2
info:
title: ToneAPI
version: '2.0'
contact:
name: Legonzaur
url: 'https://github.com/Legonzaur'
license:
name: The Unlicense
url: 'https://unlicense.org/'
description: |-
Stats tracking API for Titanfall 2 Northstar
All query params can be negated using `!`. Example : (`https://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
View File
@@ -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%",
+2422 -4944
View File
File diff suppressed because it is too large Load Diff
+11 -12
View File
@@ -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"
}
}
+134
View File
@@ -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
+51
View File
@@ -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)
})
}
})()
})
Submodule src/db deleted from b8a684acfa
+62
View File
@@ -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
+52
View File
@@ -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;
+104
View File
@@ -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> {
}
+73
View File
@@ -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
+56
View File
@@ -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
}
+43
View File
@@ -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
+282
View File
@@ -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
View File
@@ -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)
-114
View File
@@ -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
-171
View File
@@ -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
-34
View File
@@ -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
-83
View File
@@ -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
-85
View File
@@ -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
View File
@@ -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
}
+85
View File
@@ -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
})
)
}
})
})
})
+68
View File
@@ -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()
})
})
+161
View File
@@ -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
View File
@@ -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()
})
})
// })
})
-105
View File
@@ -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
View File
@@ -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"]
}