diff --git a/.gitignore b/.gitignore index e9abb48..13cb7d3 100644 --- a/.gitignore +++ b/.gitignore @@ -223,4 +223,8 @@ fabric.properties # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser -data/db.sqlite \ No newline at end of file +# SQLite +data/db.sqlite +data/db.sqlite-shm +data/db.sqlite-wal +data/database-backups \ No newline at end of file diff --git a/data/config.json b/data/config.json index 41fcafa..0d42e3d 100644 --- a/data/config.json +++ b/data/config.json @@ -6,5 +6,8 @@ "scanner": { "updateCron": "*/1 * * * *", "timeout": 2000 + }, + "backup": { + "cron": "0 0 * * *" } } diff --git a/package.json b/package.json index e2330bf..c8bd45c 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b13260..bc79e89 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'} diff --git a/src/database/database.ts b/src/database/database.ts new file mode 100644 index 0000000..eb395ba --- /dev/null +++ b/src/database/database.ts @@ -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 + } +} diff --git a/src/index.ts b/src/index.ts index e16a79c..f50f9c5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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}`); +}); diff --git a/src/scanner/scanner.ts b/src/scanner/scanner.ts index 0f429f0..047ea16 100644 --- a/src/scanner/scanner.ts +++ b/src/scanner/scanner.ts @@ -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 { - 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 { - //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); } } diff --git a/src/server/server.ts b/src/server/server.ts index 0bf58b5..1336a0b 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -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 { - 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. * diff --git a/src/types/ping.ts b/src/types/ping.ts new file mode 100644 index 0000000..ab23af3 --- /dev/null +++ b/src/types/ping.ts @@ -0,0 +1,6 @@ +export type Ping = { + id: number; + timestamp: number; + ip: string; + playerCount: number; +}; diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..4063700 --- /dev/null +++ b/src/utils/logger.ts @@ -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), + }), + ], +}); diff --git a/src/utils/timeUtils.ts b/src/utils/timeUtils.ts new file mode 100644 index 0000000..938ecad --- /dev/null +++ b/src/utils/timeUtils.ts @@ -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); +}