This commit is contained in:
Lee 2024-01-01 17:04:19 +00:00
parent 3750a00017
commit a3dbd9e689
11 changed files with 476 additions and 104 deletions

6
.gitignore vendored

@ -223,4 +223,8 @@ fabric.properties
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
data/db.sqlite
# SQLite
data/db.sqlite
data/db.sqlite-shm
data/db.sqlite-wal
data/database-backups

@ -6,5 +6,8 @@
"scanner": {
"updateCron": "*/1 * * * *",
"timeout": 2000
},
"backup": {
"cron": "0 0 * * *"
}
}

@ -15,10 +15,12 @@
"@types/node-cron": "^3.0.11",
"better-sqlite3": "^9.2.2",
"dns": "^0.2.2",
"mcpe-ping-fixed": "^0.0.3",
"mcping-js": "^1.5.0",
"node-cron": "^3.0.3",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"winston": "^3.11.0"
},
"devDependencies": {
"@types/node": "^20.10.6"

199
pnpm-lock.yaml generated

@ -20,6 +20,9 @@ dependencies:
dns:
specifier: ^0.2.2
version: 0.2.2
mcpe-ping-fixed:
specifier: ^0.0.3
version: 0.0.3
mcping-js:
specifier: ^1.5.0
version: 1.5.0
@ -32,6 +35,9 @@ dependencies:
typescript:
specifier: ^5.3.3
version: 5.3.3
winston:
specifier: ^3.11.0
version: 3.11.0
devDependencies:
'@types/node':
@ -40,6 +46,11 @@ devDependencies:
packages:
/@colors/colors@1.6.0:
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
engines: {node: '>=0.1.90'}
dev: false
/@cspotcode/source-map-support@0.8.1:
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
@ -47,6 +58,14 @@ packages:
'@jridgewell/trace-mapping': 0.3.9
dev: false
/@dabh/diagnostics@2.0.3:
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
dependencies:
colorspace: 1.1.4
enabled: 2.0.0
kuler: 2.0.0
dev: false
/@jridgewell/resolve-uri@3.1.1:
resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==}
engines: {node: '>=6.0.0'}
@ -98,6 +117,10 @@ packages:
dependencies:
undici-types: 5.26.5
/@types/triple-beam@1.3.5:
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
dev: false
/accepts@1.0.7:
resolution: {integrity: sha512-iq8ew2zitUlNcUca0wye3fYwQ6sSPItDo38oC0R+XA5KTzeXRN+GF7NjOXs3dVItj4J+gQVdpq4/qbnMb1hMHw==}
engines: {node: '>= 0.8.0'}
@ -138,6 +161,14 @@ packages:
resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==}
dev: false
/async@0.9.0:
resolution: {integrity: sha512-XQJ3MipmCHAIBBMFfu2jaSetneOrXbSyyqeU3Nod867oNOpS+i9FEms5PWgjMxSgBybRf2IVVLtr1YfrDO+okg==}
dev: false
/async@3.2.5:
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
dev: false
/aws-sign@0.2.0:
resolution: {integrity: sha512-6P7/Ls5F6++DsKu7iacris7qq/AZSWaX+gT4dtSyUxM82ePxWxaP7Slo82ZO3ZTx6GSKxQHAQlmFvM8e+Dd8ZA==}
dev: false
@ -236,6 +267,19 @@ packages:
verror: 1.10.1
dev: false
/bufferview@1.0.1:
resolution: {integrity: sha512-q87jdvsZ/sEngmDUvPT/PJsBGCi998c3B1U/6IN1uGg+R2HrTFJUDccXZEx6OxpuLySyBDGXc7vkSt4BXTyKxA==}
engines: {node: '>=0.8'}
dev: false
/bytebuffer@4.1.0:
resolution: {integrity: sha512-iUP8IfllRZiCGYACmcE7IxEfW+L1OKUEcHhXsrosqf51HnwR55THbePWeY3xAFxMlhhUa2I6x3cp5zG2vHI2YQ==}
engines: {node: '>=0.8'}
dependencies:
bufferview: 1.0.1
long: 2.4.0
dev: false
/bytes@1.0.0:
resolution: {integrity: sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==}
dev: false
@ -248,11 +292,46 @@ packages:
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
dev: false
/color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
dependencies:
color-name: 1.1.3
dev: false
/color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
dev: false
/color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: false
/color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
dev: false
/color@3.2.1:
resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==}
dependencies:
color-convert: 1.9.3
color-string: 1.9.1
dev: false
/colors@0.6.2:
resolution: {integrity: sha512-OsSVtHK8Ir8r3+Fxw/b4jS1ZLPXkV6ZxDRJQzeD7qo0SqMXWrHDM71DgYzPMHY8SFJ0Ao+nNU2p1MmwdzKqPrw==}
engines: {node: '>=0.1.90'}
dev: false
/colorspace@1.1.4:
resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
dependencies:
color: 3.2.1
text-hex: 1.0.0
dev: false
/combined-stream@0.0.7:
resolution: {integrity: sha512-qfexlmLp9MyrkajQVyjEDb0Vj+KhRgR/rxLiVhaihlT+ZkX0lReqtH6Ack40CvMDERR4b5eFp3CreskpBs1Pig==}
engines: {node: '>= 0.8'}
@ -425,6 +504,10 @@ packages:
resolution: {integrity: sha512-1q/3kz+ZwmrrWpJcCCrBZ3JnBzB1BMA5EVW9nxnIP1LxDZ16Cqs9VdolqLWlExet1vU+bar3WSkAa4/YrA9bIw==}
dev: false
/enabled@2.0.0:
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
dev: false
/end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
dependencies:
@ -531,6 +614,10 @@ packages:
engines: {node: '> 0.1.90'}
dev: false
/fecha@4.2.3:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
dev: false
/file-uri-to-path@1.0.0:
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
dev: false
@ -561,6 +648,10 @@ packages:
ee-first: 1.0.3
dev: false
/fn.name@1.1.0:
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
dev: false
/forever-agent@0.2.0:
resolution: {integrity: sha512-IasWSRIlfPnBZY1K9jEUK3PwsScR4mrcK+aNBJzGoPnW+S9b6f8I8ScyH4cehEOFNqnjGpP2gCaA22gqSV1xQA==}
dev: false
@ -654,6 +745,15 @@ packages:
engines: {node: '>= 10'}
dev: false
/is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
dev: false
/is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
dev: false
/isarray@0.0.1:
resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
dev: false
@ -667,6 +767,27 @@ packages:
deprecated: Please use the native JSON object instead of JSON 3
dev: false
/kuler@2.0.0:
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
dev: false
/logform@2.6.0:
resolution: {integrity: sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==}
engines: {node: '>= 12.0.0'}
dependencies:
'@colors/colors': 1.6.0
'@types/triple-beam': 1.3.5
fecha: 4.2.3
ms: 2.1.3
safe-stable-stringify: 2.4.3
triple-beam: 1.4.1
dev: false
/long@2.4.0:
resolution: {integrity: sha512-ijUtjmO/n2A5PaosNG9ZGDsQ3vxJg7ZW8vsY8Kp0f2yIZWhSJvjmegV7t+9RPQKxKrvj8yKGehhS+po14hPLGQ==}
engines: {node: '>=0.6'}
dev: false
/lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
@ -678,6 +799,13 @@ packages:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
dev: false
/mcpe-ping-fixed@0.0.3:
resolution: {integrity: sha512-STb0fhDpFB6Z2JJGFRHIG0yat3vyI3U6QVatyY5rLlrbcfw+elP4NcpfXp9pmr5N9kQff2Chw1xsRW9sI9CXPQ==}
dependencies:
bytebuffer: 4.1.0
portfinder: 0.4.0
dev: false
/mcping-js@1.5.0:
resolution: {integrity: sha512-CoXbpbSqE4OtJupU2lvzoKFrGerH0AoyY9YoQ98EKUOhtxfLQa+f+DFqWYUaZ451Scru1gJm/+w6d3IKJhMGvQ==}
dev: false
@ -717,6 +845,13 @@ packages:
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
dev: false
/mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
dependencies:
minimist: 1.2.8
dev: false
/morgan@1.2.0:
resolution: {integrity: sha512-VrasIzA69dsxJm1+MVWTLTiij3kiG33XPfGiexqstHpcSvSu/Z51W+FGQyIlbc3jZZuF2PFujsjw+YQvpXz3UA==}
engines: {node: '>= 0.8.0'}
@ -731,6 +866,10 @@ packages:
resolution: {integrity: sha512-/pc3eh7TWorTtbvXg8je4GvrvEqCfH7PA3P7iW01yL2E53FKixzgMBaQi0NOPbMJqY34cBSvR0tZtmlTkdUG4A==}
dev: false
/ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
dev: false
/nan@0.3.2:
resolution: {integrity: sha512-V9/Pyy5Oelv6vVJP9X+dAzU3IO19j6YXrJnODHxP2h54hTvfFQGahdsQV6Ule/UukiEJk1SkQ/aUyWUm61RBQw==}
dev: false
@ -807,6 +946,12 @@ packages:
wrappy: 1.0.2
dev: false
/one-time@1.0.0:
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
dependencies:
fn.name: 1.1.0
dev: false
/optimist@0.3.7:
resolution: {integrity: sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==}
dependencies:
@ -849,6 +994,14 @@ packages:
engines: {node: '>= 0.4.0'}
dev: false
/portfinder@0.4.0:
resolution: {integrity: sha512-SZ3hp61WVhwNSS0gf0Fdrx5Yb/wl35qisHuPVM1S0StV8t5XlVZmmJy7/417OELJA7t6ecEmeEzvOaBwq3kCiQ==}
engines: {node: '>= 0.8.0'}
dependencies:
async: 0.9.0
mkdirp: 0.5.6
dev: false
/prebuild-install@7.1.1:
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
engines: {node: '>=10'}
@ -943,6 +1096,11 @@ packages:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: false
/safe-stable-stringify@2.4.3:
resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==}
engines: {node: '>=10'}
dev: false
/semver@7.5.4:
resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
engines: {node: '>=10'}
@ -990,6 +1148,12 @@ packages:
simple-concat: 1.0.1
dev: false
/simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
dependencies:
is-arrayish: 0.3.2
dev: false
/sntp@0.1.4:
resolution: {integrity: sha512-v90tkW8VIdXwY35BJAWIpZWd/h+WC7TufizgUO2jtOY21isIo8IP85f1zJ8mKF8o77Vxo5k+GJmUZ4H6phVt1g==}
engines: {node: 0.8.x}
@ -1098,6 +1262,10 @@ packages:
readable-stream: 3.6.2
dev: false
/text-hex@1.0.0:
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
dev: false
/tinycolor@0.0.1:
resolution: {integrity: sha512-+CorETse1kl98xg0WAzii8DTT4ABF4R3nquhrkIbVGcw1T8JYs5Gfx9xEfGINPUZGDj9C4BmOtuKeaTtuuRolg==}
engines: {node: '>=0.4.0'}
@ -1130,6 +1298,11 @@ packages:
- utf-8-validate
dev: false
/triple-beam@1.4.1:
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
engines: {node: '>= 14.0.0'}
dev: false
/ts-node@10.9.2(@types/node@20.10.6)(typescript@5.3.3):
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
hasBin: true
@ -1224,6 +1397,15 @@ packages:
extsprintf: 1.4.1
dev: false
/winston-transport@4.6.0:
resolution: {integrity: sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==}
engines: {node: '>= 12.0.0'}
dependencies:
logform: 2.6.0
readable-stream: 3.6.2
triple-beam: 1.4.1
dev: false
/winston@0.7.3:
resolution: {integrity: sha512-iVTT8tf9YnTyfZX+aEUj2fl6WBRet7za6vdjMeyF8SA80Vii2rreM5XH+5qmpBV9uJGj8jz8BozvTDcroVq/eA==}
engines: {node: '>= 0.6.0'}
@ -1237,6 +1419,23 @@ packages:
stack-trace: 0.0.10
dev: false
/winston@3.11.0:
resolution: {integrity: sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==}
engines: {node: '>= 12.0.0'}
dependencies:
'@colors/colors': 1.6.0
'@dabh/diagnostics': 2.0.3
async: 3.2.5
is-stream: 2.0.1
logform: 2.6.0
one-time: 1.0.0
readable-stream: 3.6.2
safe-stable-stringify: 2.4.3
stack-trace: 0.0.10
triple-beam: 1.4.1
winston-transport: 4.6.0
dev: false
/wordwrap@0.0.3:
resolution: {integrity: sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==}
engines: {node: '>=0.4.0'}

153
src/database/database.ts Normal file

@ -0,0 +1,153 @@
import SQLiteDatabase from "better-sqlite3";
import cron from "node-cron";
import Server, { PingResponse } from "../server/server";
import { logger } from "../utils/logger";
import { getFormattedDate } from "../utils/timeUtils";
import Config from "../../data/config.json";
import { Ping } from "../types/ping";
import { createDirectory } from "../utils/fsUtils";
const DATA_DIR = "data";
const BACKUP_DIR = `${DATA_DIR}/database-backups`;
const PINGS_TABLE = "pings";
const RECORD_TABLE = "record";
/**
* SQL Queries
*/
const CREATE_PINGS_TABLE = `
CREATE TABLE IF NOT EXISTS pings (
id INTEGER NOT NULL,
timestamp BIGINT NOT NULL,
ip TINYTEXT NOT NULL,
playerCount MEDIUMINT NOT NULL
);
`;
const CREATE_RECORD_TABLE = `
CREATE TABLE IF NOT EXISTS record (
id INTEGER PRIMARY KEY,
timestamp BIGINT NOT NULL,
ip TINYTEXT NOT NULL,
playerCount MEDIUMINT NOT NULL
);
`;
const CREATE_PINGS_INDEX = `CREATE INDEX IF NOT EXISTS ip_index ON pings (id, ip, playerCount)`;
const CREATE_TIMESTAMP_INDEX = `CREATE INDEX IF NOT EXISTS timestamp_index on PINGS (id, timestamp)`;
const INSERT_PING = `
INSERT INTO ${PINGS_TABLE} (id, timestamp, ip, playerCount)
VALUES (?, ?, ?, ?)
`;
const INSERT_RECORD = `
INSERT INTO ${RECORD_TABLE} (id, timestamp, ip, playerCount)
VALUES (?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
timestamp = excluded.timestamp,
playerCount = excluded.playerCount,
ip = excluded.ip
`;
const SELECT_PINGS = `
SELECT * FROM ${PINGS_TABLE} WHERE id = ? AND timestamp >= ? AND timestamp <= ?
`;
const SELECT_RECORD = `
SELECT playerCount, timestamp FROM ${RECORD_TABLE} WHERE {} = ?
`;
const SELECT_RECORD_BY_ID = SELECT_RECORD.replace("{}", "id");
const SELECT_RECORD_BY_IP = SELECT_RECORD.replace("{}", "ip");
export default class Database {
private db: SQLiteDatabase.Database;
constructor() {
this.db = new SQLiteDatabase(`${DATA_DIR}/db.sqlite`);
this.db.pragma("journal_mode = WAL");
logger.info("Ensuring tables exist");
this.db.exec(CREATE_PINGS_TABLE); // Ensure the pings table exists
this.db.exec(CREATE_RECORD_TABLE); // Ensure the record table exists
logger.info("Ensuring indexes exist");
this.db.exec(CREATE_PINGS_INDEX); // Ensure the pings index exists
this.db.exec(CREATE_TIMESTAMP_INDEX); // Ensure the timestamp index exists
cron.schedule(Config.backup.cron, () => {
this.createBackup();
});
}
/**
* Gets the pings for a server.
*
* @param id the server ID
* @param startTime the start time
* @param endTime the end time
* @returns the pings for the server
*/
public getPings(id: number, startTime: number, endTime: number): Ping[] | [] {
return this.db.prepare(SELECT_PINGS).all(id, startTime, endTime) as
| Ping[]
| [];
}
/**
* Gets the record player count for a server.
*
* @param value the server ID or IP
* @returns the record for the server
*/
public getRecord(value: any): Ping | undefined {
if (typeof value === "number") {
return this.db.prepare(SELECT_RECORD_BY_ID).get(value) as
| Ping
| undefined;
}
return this.db.prepare(SELECT_RECORD_BY_IP).get(value) as Ping | undefined;
}
/**
* Creates a full backup of the database.
*/
public async createBackup() {
logger.info("Creating database backup");
createDirectory(BACKUP_DIR);
await this.db.backup(`${BACKUP_DIR}/${getFormattedDate()}.sqlite`);
logger.info("Finished creating database backup");
}
/**
* Inserts a ping into the database.
*
* @param timestamp the timestamp of the ping
* @param ip the IP address of the server
* @param playerCount the number of players online
*/
public insertPing(server: Server, response: PingResponse) {
const { timestamp, players } = response;
const id = server.getID();
const ip = server.getIP();
const onlineCount = players.online;
const statement = this.db.prepare(INSERT_PING);
statement.run(id, timestamp, ip, onlineCount); // Insert the ping into the database
}
/**
* Inserts a record into the database.
*
* @param server the server to insert
* @param response the response to insert
*/
public insertRecord(server: Server, response: PingResponse) {
const { timestamp, players } = response;
const id = server.getID();
const ip = server.getIP();
const onlineCount = players.online;
const statement = this.db.prepare(INSERT_RECORD);
statement.run(id, timestamp, ip, onlineCount); // Insert the record into the database
}
}

@ -1,5 +1,21 @@
import Database from "./database/database";
import Scanner from "./scanner/scanner";
import ServerManager from "./server/serverManager";
/**
* The database instance.
*/
export const database = new Database();
/**
* The server manager instance.
*/
export const serverManager = new ServerManager();
// The scanner is responsible for scanning all servers
new Scanner();
serverManager.getServers().forEach((server) => {
const record = database.getRecord(server.getID());
console.log(`Record for "${server.getName()}": ${record?.playerCount}`);
});

@ -1,67 +1,15 @@
import Database from "better-sqlite3";
import cron from "node-cron";
import { serverManager } from "..";
import { database, serverManager } from "..";
import Server from "../server/server";
import Config from "../../data/config.json";
import Server, { PingResponse } from "../server/server";
const DATA_DIR = "data";
const PINGS_TABLE = "pings";
const RECORD_TABLE = "record";
/**
* SQL Queries
*/
const CREATE_PINGS_TABLE = `
CREATE TABLE IF NOT EXISTS pings (
id INTEGER NOT NULL,
timestamp BIGINT NOT NULL,
ip TINYTEXT NOT NULL,
player_count MEDIUMINT NOT NULL
);
`;
const CREATE_RECORD_TABLE = `
CREATE TABLE IF NOT EXISTS record (
id INTEGER PRIMARY KEY,
timestamp BIGINT NOT NULL,
ip TINYTEXT NOT NULL,
player_count MEDIUMINT NOT NULL
);
`;
const CREATE_PINGS_INDEX = `CREATE INDEX IF NOT EXISTS ip_index ON pings (id, ip, player_count)`;
const CREATE_TIMESTAMP_INDEX = `CREATE INDEX IF NOT EXISTS timestamp_index on PINGS (id, timestamp)`;
const INSERT_PING = `
INSERT INTO ${PINGS_TABLE} (id, timestamp, ip, player_count)
VALUES (?, ?, ?, ?)
`;
const INSERT_RECORD = `
INSERT INTO ${RECORD_TABLE} (id, timestamp, ip, player_count)
VALUES (?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
timestamp = excluded.timestamp,
player_count = excluded.player_count,
ip = excluded.ip
`;
import { logger } from "../utils/logger";
export default class Scanner {
private db: Database.Database;
constructor() {
console.log("Loading scanner database");
this.db = new Database(`${DATA_DIR}/db.sqlite`);
logger.info("Loading scanner database");
console.log("Ensuring tables exist");
this.db.exec(CREATE_PINGS_TABLE); // Ensure the pings table exists
this.db.exec(CREATE_RECORD_TABLE); // Ensure the record table exists
console.log("Ensuring indexes exist");
this.db.exec(CREATE_PINGS_INDEX); // Ensure the pings index exists
this.db.exec(CREATE_TIMESTAMP_INDEX); // Ensure the timestamp index exists
console.log("Starting server scan");
logger.info("Starting server scan");
cron.schedule(Config.scanner.updateCron, () => {
this.scanServers();
});
@ -71,14 +19,14 @@ export default class Scanner {
* Start a server scan to ping all servers.
*/
private async scanServers(): Promise<void> {
console.log(`Scanning servers ${serverManager.getServers().length}`);
logger.info(`Scanning servers ${serverManager.getServers().length}`);
// ping all servers in parallel
await Promise.all(
serverManager.getServers().map((server) => this.scanServer(server))
);
console.log("Finished scanning servers");
logger.info("Finished scanning servers");
}
/**
@ -88,7 +36,7 @@ export default class Scanner {
* @returns a promise that resolves when the server has been scanned
*/
async scanServer(server: Server): Promise<void> {
//console.log(`Scanning server ${server.getIP()} - ${server.getType()}`);
//logger.info(`Scanning server ${server.getIP()} - ${server.getType()}`);
let response;
let online = false;
@ -99,7 +47,7 @@ export default class Scanner {
}
online = true;
} catch (err) {
console.log(`Failed to ping ${server.getIP()}`, err);
logger.info(`Failed to ping ${server.getIP()}`, err);
return;
}
@ -107,40 +55,7 @@ export default class Scanner {
return; // Server is offline
}
this.insertPing(server, response);
this.insertRecord(server, response);
}
/**
* Inserts a ping into the database.
*
* @param timestamp the timestamp of the ping
* @param ip the IP address of the server
* @param playerCount the number of players online
*/
private insertPing(server: Server, response: PingResponse): void {
const { timestamp, players } = response;
const id = server.getID();
const ip = server.getIP();
const onlineCount = players.online;
const statement = this.db.prepare(INSERT_PING);
statement.run(id, timestamp, ip, onlineCount); // Insert the ping into the database
}
/**
* Inserts a record into the database.
*
* @param server the server to insert
* @param response the response to insert
*/
private insertRecord(server: Server, response: PingResponse): void {
const { timestamp, players } = response;
const id = server.getID();
const ip = server.getIP();
const onlineCount = players.online;
const statement = this.db.prepare(INSERT_RECORD);
statement.run(id, timestamp, ip, onlineCount); // Insert the record into the database
database.insertPing(server, response);
database.insertRecord(server, response);
}
}

@ -1,5 +1,6 @@
import javaPing from "mcping-js";
import { ResolvedServer, resolveDns } from "../utils/dnsResolver";
import JavaPing = require("mcping-js");
const bedrockPing = require("mcpe-ping-fixed"); // Doesn't have typescript definitions
import Config from "../../data/config.json";
@ -16,10 +17,10 @@ export type ServerType = "PC" | "PE";
export type PingResponse = {
timestamp: number;
ip: string;
version: string;
version?: string;
players: {
online: number;
max: number;
max?: number;
};
};
@ -27,6 +28,7 @@ type ServerOptions = {
id: number;
name: string;
ip: string;
port?: number;
type: ServerType;
};
@ -51,6 +53,11 @@ export default class Server {
*/
private ip: string;
/**
* The port of the server.
*/
private port: number | undefined;
/**
* The type of server.
*/
@ -64,10 +71,11 @@ export default class Server {
hasResolved: false,
};
constructor({ id, name, ip, type }: ServerOptions) {
constructor({ id, name, ip, port, type }: ServerOptions) {
this.id = id;
this.name = name;
this.ip = ip;
this.port = port;
this.type = type;
}
@ -126,7 +134,7 @@ export default class Server {
port = 25565; // The default port
}
const serverPing = new JavaPing.MinecraftServer(ip, port);
const serverPing = new javaPing.MinecraftServer(ip, port);
return new Promise((resolve, reject) => {
serverPing.ping(Config.scanner.timeout, 700, (err, res) => {
@ -156,7 +164,25 @@ export default class Server {
private async pingPEServer(
server: Server
): Promise<PingResponse | undefined> {
return undefined;
return new Promise((resolve, reject) => {
bedrockPing(
server.getIP(),
server.getPort() || 19132,
(err: any, res: any) => {
if (err || res == undefined) {
return reject(err);
}
resolve({
timestamp: Date.now(),
ip: server.getIP(),
players: {
online: res.currentPlayers,
},
});
}
);
});
}
/**
@ -186,6 +212,15 @@ export default class Server {
return this.ip;
}
/**
* Returns the port of the server.
*
* @returns the port
*/
public getPort(): number | undefined {
return this.port;
}
/**
* Returns the type of server.
*

6
src/types/ping.ts Normal file

@ -0,0 +1,6 @@
export type Ping = {
id: number;
timestamp: number;
ip: string;
playerCount: number;
};

31
src/utils/logger.ts Normal file

@ -0,0 +1,31 @@
import Winston, { format } from "winston";
const { colorize, timestamp, printf } = format;
interface LogInfo {
level: string;
message: string;
label?: string;
timestamp?: string;
}
const customFormat = format.combine(
timestamp({ format: "YY-MM-DD HH:MM:SS" }),
printf((info: LogInfo) => {
return `[${info.timestamp}] ${info.level}: ${info.message}`;
})
);
/**
* The global logger instance.
*/
export const logger = Winston.createLogger({
transports: [
new Winston.transports.Console({
format: Winston.format.combine(colorize(), customFormat),
}),
new Winston.transports.File({
filename: `data/logs/${new Date().toISOString().slice(0, 10)}.log`,
format: Winston.format.combine(customFormat),
}),
],
});

8
src/utils/timeUtils.ts Normal file

@ -0,0 +1,8 @@
/**
* Gets the current date as YYYY-MM-DD.
*
* @returns the date
*/
export function getFormattedDate() {
return new Date().toISOString().slice(0, 10);
}