diff --git a/package-lock.json b/package-lock.json index b6ef116..5282a14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,11 @@ "discord.js": "^14.9.0", "dotenv": "^16.0.3", "express": "^4.18.2", - "firebase-admin": "^11.7.0" + "express-async-errors": "^3.1.1", + "firebase-admin": "^11.7.0", + "pino": "^8.14.1", + "pino-pretty": "^10.0.1", + "uuid": "^9.0.0" }, "devDependencies": { "eslint": "^8.38.0", @@ -686,7 +690,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "optional": true, "dependencies": { "event-target-shim": "^5.0.0" }, @@ -821,11 +824,18 @@ "retry": "0.13.1" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", @@ -844,8 +854,7 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "optional": true + ] }, "node_modules/bignumber.js": { "version": "9.1.1", @@ -929,6 +938,29 @@ "node": ">=8" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -1070,6 +1102,11 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "devOptional": true }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", @@ -1146,6 +1183,14 @@ "node": ">= 8" } }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1276,7 +1321,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "optional": true, "dependencies": { "once": "^1.4.0" } @@ -1573,11 +1617,18 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "optional": true, "engines": { "node": ">=6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -1619,6 +1670,14 @@ "node": ">= 0.10.0" } }, + "node_modules/express-async-errors": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz", + "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==", + "peerDependencies": { + "express": "^4.16.2" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1638,6 +1697,11 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "optional": true }, + "node_modules/fast-copy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz", + "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1655,6 +1719,19 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "devOptional": true }, + "node_modules/fast-redact": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.2.0.tgz", + "integrity": "sha512-zaTadChr+NekyzallAMXATXLOR8MNx3zqpZ0MUF2aGf4EathnG0f32VLODNlY8IuGY3HoRO2L6/6fSzNsLaHIw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, "node_modules/fast-text-encoding": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz", @@ -1827,8 +1904,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "devOptional": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { "version": "2.3.2", @@ -2074,6 +2150,52 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/help-me": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", + "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", + "dependencies": { + "glob": "^8.0.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/help-me/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/help-me/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/help-me/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -2195,7 +2317,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "devOptional": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2306,6 +2427,14 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "engines": { + "node": ">=10" + } + }, "node_modules/js-sdsl": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", @@ -2711,7 +2840,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "optional": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2882,6 +3010,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz", + "integrity": "sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -2897,7 +3030,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "devOptional": true, "dependencies": { "wrappy": "1" } @@ -3025,6 +3157,95 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pino": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.14.1.tgz", + "integrity": "sha512-8LYNv7BKWXSfS+k6oEc6occy5La+q2sPwU3q2ljTX5AZk7v+5kND2o5W794FyRaqha6DJajmkNRsWtPpFyMUdw==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.0.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.1.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", + "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.0.1.tgz", + "integrity": "sha512-yrn00+jNpkvZX/NrPVCPIVHAfTDy3ahF0PND9tKqZk4j9s+loK8dpzrJj4dGb7i+WLuR50ussuTAiWoMWU+qeA==", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^4.0.1", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3049,6 +3270,19 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.2.0.tgz", + "integrity": "sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==" + }, "node_modules/proto3-json-serializer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-1.1.0.tgz", @@ -3197,6 +3431,15 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -3240,6 +3483,11 @@ } ] }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -3302,6 +3550,14 @@ "node": ">=8.10.0" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3418,11 +3674,24 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, "node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -3558,6 +3827,14 @@ "semver": "bin/semver.js" } }, + "node_modules/sonic-boom": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.3.0.tgz", + "integrity": "sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3567,6 +3844,14 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -3636,7 +3921,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "devOptional": true, "engines": { "node": ">=8" }, @@ -3705,6 +3989,14 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/thread-stream": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.3.0.tgz", + "integrity": "sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -3985,8 +4277,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { "version": "8.13.0", diff --git a/package.json b/package.json index 4fc45c2..64d6c12 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,10 @@ "discord.js": "^14.9.0", "dotenv": "^16.0.3", "express": "^4.18.2", - "firebase-admin": "^11.7.0" + "express-async-errors": "^3.1.1", + "firebase-admin": "^11.7.0", + "pino": "^8.14.1", + "pino-pretty": "^10.0.1", + "uuid": "^9.0.0" } -} \ No newline at end of file +} diff --git a/src/apiServer/apiServer.js b/src/apiServer/apiServer.js new file mode 100644 index 0000000..67f4455 --- /dev/null +++ b/src/apiServer/apiServer.js @@ -0,0 +1,37 @@ +const app = require('./express'); +const http = require('http'); + +const logger = require('../lib/logger.js'); + +class ApiServer { + constructor(port) { + logger.info('Configure API Server ...', { port, node_env: process.env.NODE_ENV }); + + this.port = port; + this.httpServer = null; + this.db = null; + + this.#createServer(); + this.#bindEvents(); + } + + #createServer() { + this.httpServer = http.createServer(app); + } + + #bindEvents() { + logger.info('Binding Events ...'); + this.httpServer.on('listening', () => { + logger.info(`Server started on port: ${this.port} for NODE_ENV:${process.env.NODE_ENV}`); + }); + this.httpServer.on('error', (err) => { + logger.error('Server error:', err); + }); + } + + start() { + this.httpServer.listen(this.port); + } +} + +module.exports = ApiServer; \ No newline at end of file diff --git a/src/apiServer/express.js b/src/apiServer/express.js new file mode 100644 index 0000000..fb8c9e1 --- /dev/null +++ b/src/apiServer/express.js @@ -0,0 +1,33 @@ +const express = require('express'); +require('express-async-errors'); // for catching async error +const cors = require('cors'); +const router = require('./routes/index.js'); +const errorHandler = require('./middlewares/errorHandler.js'); +const resMethod = require('./middlewares/resMethod.js'); +const reqInitializer = require('./middlewares/reqInitializer.js'); + +const app = express(); + +app.use(cors({ + exposedHeaders: ['X-Request-Id'], +})); + + +app.use(reqInitializer); +app.use(resMethod); +app.use('/api', router); + +app.use(errorHandler); + +// 404 handler +app.use('*', (req, res) => res.status(404).send({ + success: false, + message: '指定路徑並不存在', + error: { + code: 404, + type: 'RouteNotFound', + debugInfo: {}, + }, +})); + +module.exports = app; \ No newline at end of file diff --git a/src/apiServer/middlewares/errorHandler.js b/src/apiServer/middlewares/errorHandler.js new file mode 100644 index 0000000..f7f4d8d --- /dev/null +++ b/src/apiServer/middlewares/errorHandler.js @@ -0,0 +1,26 @@ +const logger = require('../../lib/logger.js'); +const Errors = require('../../lib/Errors.js'); + +/** + * @type {import('express').ErrorRequestHandler} + */ +module.exports = function errorHandler(err, req, res, next) { + const errorMessage = err.msg || err.message; + const data = {}; + if (process.env.NODE_ENV === 'dev') { + data.stack = err.stack; + console.log(err); + } + + logger.info(`General error handler for error: ${errorMessage}`, data); + if (res.headersSent) { + return next(err); + } else if (err instanceof Errors.GeneralError) { + return res.fail(err); + } + + // 當錯誤並非預定義時 + logger.error({ msg: 'Exception', errMsg: err.message, errName: err.name }); + res.status(501); + return res.send(err.message); +}; \ No newline at end of file diff --git a/src/apiServer/middlewares/reqInitializer.js b/src/apiServer/middlewares/reqInitializer.js new file mode 100644 index 0000000..4d1bd9c --- /dev/null +++ b/src/apiServer/middlewares/reqInitializer.js @@ -0,0 +1,31 @@ +const { v4: uuidv4 } = require('uuid'); +const logger = require('../../lib/logger'); + +/** + * @type {import('express').Handler} + */ +module.exports = function reqInitializer(req, res, next) { + const start = process.hrtime.bigint(); + req.requestId = uuidv4(); + + const { method, query = {}, body = {}, requestId, originalUrl } = req; + logger.debug({ msg: `${method} ${originalUrl} [STARTED]`, requestId, query, body }); + + res.on('finish', () => { + const durationInMilliseconds = getDurationInMilliseconds(start); + logger.info({ msg: `${method} ${originalUrl} [FINISHED] ${durationInMilliseconds.toLocaleString()} ms`, requestId }); + }); + + res.on('close', () => { + const durationInMilliseconds = getDurationInMilliseconds(start); + logger.info({ msg: `${method} ${originalUrl} [CLOSED] ${durationInMilliseconds.toLocaleString()} ms`, requestId }); + }); + + res.setHeader('X-Request-Id', req.requestId); + next(); +}; + +function getDurationInMilliseconds(start) { + const diff = process.hrtime.bigint() - start; + return Number(diff) / 1000000; +} \ No newline at end of file diff --git a/src/apiServer/middlewares/resMethod.js b/src/apiServer/middlewares/resMethod.js new file mode 100644 index 0000000..1bf467c --- /dev/null +++ b/src/apiServer/middlewares/resMethod.js @@ -0,0 +1,29 @@ +/** + * @type {import('express').Handler} + */ +module.exports = function resMethod(req, res, next) { + res.success = function success({ data, ...info }) { + res.status(200).send({ + success: true, + data, + ...info, + }); + }; + + /** + * @param {Error} error + */ + res.fail = function fail(error) { + res.status(error.code).send({ + success: false, + message: error.message, + error: { + code: error.code, + type: error.name, + debugInfo: error.data, + } + }); + }; + + next(); +}; \ No newline at end of file diff --git a/src/apiServer/routes/index.js b/src/apiServer/routes/index.js new file mode 100644 index 0000000..de2b59c --- /dev/null +++ b/src/apiServer/routes/index.js @@ -0,0 +1,10 @@ +const express = require('express'); +const router = express.Router(); + +router.use('/leaderboard', require('./leaderboard.js')); +router.get('/healthCheck', (req, res) => { + return res.success({ data: 'ok' }); +}); + + +module.exports = router; \ No newline at end of file diff --git a/src/apiServer/routes/leaderboard.js b/src/apiServer/routes/leaderboard.js new file mode 100644 index 0000000..4fd549d --- /dev/null +++ b/src/apiServer/routes/leaderboard.js @@ -0,0 +1,81 @@ +const express = require('express'); +const router = express.Router(); +const Discord = require('discord.js'); + +const { db } = require('../../config/db'); +const { pageSize } = require('../../const.js'); + +function validate(req, res, next) { + const { date, discordId, page } = req.query; + if (page && typeof Number(page) && Number(page) <= 0) { + return res.status(400).json('page must be valid number'); + } + + const dateRegex = /([12]\d{3}-(0[1-9]|1[0-2]))/; + if (date && !dateRegex.test(date)) { + return res.status(400).json('date must be in YYYY-MM format'); + } + + if (discordId && typeof discordId !== 'string') { + return res.status(400).json('discordId must be string'); + } + + next(); +} + + + +router.get('/', validate, async function (req, res) { + const currentYearAndMonth = new Date().toISOString().slice(0, 7); + const { date, discordId, page = 1 } = req.query; + let ref = db + .collection(`leaderboard-${currentYearAndMonth}`) + .where('period', '=', date || currentYearAndMonth); + + const countSnapshot = await ref.count().get(); + const totalDocsCount = countSnapshot.data().count; + const totalPages = Math.ceil(totalDocsCount / 10); + const offset = (page - 1) * pageSize; + + if (discordId) { + ref = ref.where('discordId', '=', discordId); + } + + const querySnapshot = await ref + .orderBy('point', 'desc') + .limit(pageSize) + .offset(offset) + .get(); + + const client = new Discord.Client({ + intents: [] + }); + client.login(process.env.DISCORD_TOKEN); + + const fetchUserDetails = async (doc) => { + const data = doc.data(); + const guild = await client.guilds.fetch(process.env.DISCORD_GUILDID); + const member = await guild.members.fetch(data.discordId); + + return { + id: doc.id, + name: member.nickname || member.user.username, + avatarURL: member.user.displayAvatarURL(), + ...data + }; + }; + + const promises = querySnapshot.docs.map(fetchUserDetails); + const results = await Promise.all(promises); + + return res.success({ + data: results, + offset, + pageSize, + totalPages, + currentPage: page, + totalDataCount: totalDocsCount + }); +}); + +module.exports = router; diff --git a/src/app.js b/src/app.js index 2e9ced2..0b21998 100644 --- a/src/app.js +++ b/src/app.js @@ -1,8 +1,5 @@ // Require the necessary discord.js classes require('dotenv').config(); -const express = require('express'); -var cors = require('cors'); -const app = express(); const { readdirSync } = require('node:fs'); const { join } = require('node:path'); const { @@ -15,6 +12,8 @@ const { } = require('discord.js'); const { db } = require('./config/db.js'); const { FieldValue } = require('firebase-admin/firestore'); +const logger = require('./lib/logger.js'); + const fs = require('fs'); const path = require('path'); const client = new Client({ @@ -53,7 +52,7 @@ for (const file of commandFiles) { if ('data' in command && 'execute' in command) { client.commands.set(command.data.name, command); } else { - console.log( + logger.info( `[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.` ); } @@ -66,7 +65,7 @@ client.on(Events.MessageReactionAdd, async (reaction, user) => { try { await reaction.fetch(); } catch (error) { - console.error('Something went wrong when fetching the message:', error); + logger.error('Something went wrong when fetching the message:', error); // Return as `reaction.message.author` may be undefined/null return; } @@ -139,10 +138,10 @@ client.on(Events.MessageReactionRemove, async (reaction, user) => { // Log in to Discord with your client's token client.login(process.env.DISCORD_TOKEN); -const apiRoutes = require('./routes/index'); - -app.use('/api', cors(), apiRoutes); -app.listen(process.env.PORT || 3306, () => { - console.log(`server is running on ${process.env.PORT || 3306}`); -}); +/** + * API Server Execution + */ +const ApiServer = require('./apiServer/apiServer.js'); +const server = new ApiServer(process.env.PORT || 3306); +server.start(); \ No newline at end of file diff --git a/src/commands/leaderboard/rank.js b/src/commands/leaderboard/rank.js index 2b721ad..a5ffc1f 100644 --- a/src/commands/leaderboard/rank.js +++ b/src/commands/leaderboard/rank.js @@ -1,6 +1,8 @@ require('dotenv').config(); const { SlashCommandBuilder } = require('discord.js'); const { db } = require('../../config/db.js'); +const logger = require('../../lib/logger.js'); + module.exports = { data: new SlashCommandBuilder() .setName('rank') @@ -46,6 +48,6 @@ async function queryUserRankAndPoint(discordId) { return { userRank, userPoint }; } catch (error) { - console.error('查詢用戶排名時發生錯誤:', error); + logger.error('查詢用戶排名時發生錯誤:', error); } } diff --git a/src/deploy-commands.js b/src/deploy-commands.js index dce896d..aea1a77 100644 --- a/src/deploy-commands.js +++ b/src/deploy-commands.js @@ -2,6 +2,8 @@ const { REST, Routes } = require('discord.js'); const fs = require('node:fs'); const path = require('node:path'); +const logger = require('./lib/logger.js'); + const commands = []; // Grab all the command files from the commands directory you created earlier const foldersPath = path.join(__dirname, 'commands'); @@ -20,7 +22,7 @@ for (const folder of commandFolders) { if ('data' in command && 'execute' in command) { commands.push(command.data.toJSON()); } else { - console.log( + logger.info( `[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.` ); } @@ -33,7 +35,7 @@ const rest = new REST().setToken(process.env.DISCORD_TOKEN); // and deploy your commands! (async () => { try { - console.log( + logger.info( `Started refreshing ${commands.length} application (/) commands.` ); @@ -46,11 +48,11 @@ const rest = new REST().setToken(process.env.DISCORD_TOKEN); { body: commands } ); - console.log( + logger.info( `Successfully reloaded ${data.length} application (/) commands.` ); } catch (error) { // And of course, make sure you catch and log any errors! - console.error(error); + logger.error(error); } })(); diff --git a/src/events/interactionCreate.js b/src/events/interactionCreate.js index 5465c1c..40d1ff4 100644 --- a/src/events/interactionCreate.js +++ b/src/events/interactionCreate.js @@ -1,4 +1,5 @@ const { Events } = require('discord.js'); +const logger = require('../lib/logger.js'); module.exports = { name: Events.InteractionCreate, @@ -8,7 +9,7 @@ module.exports = { const command = interaction.client.commands.get(interaction.commandName); if (!command) { - console.error( + logger.error( `No command matching ${interaction.commandName} was found.` ); return; @@ -17,8 +18,8 @@ module.exports = { try { await command.execute(interaction); } catch (error) { - console.error(`Error executing ${interaction.commandName}`); - console.error(error); + logger.error(`Error executing ${interaction.commandName}`); + logger.error(error); } } }; diff --git a/src/events/ready.js b/src/events/ready.js index ec9abde..e399ceb 100644 --- a/src/events/ready.js +++ b/src/events/ready.js @@ -1,9 +1,10 @@ const { Events } = require('discord.js'); +const logger = require('../lib/logger.js'); module.exports = { name: Events.ClientReady, once: true, execute(client) { - console.log(`Ready! Logged in as ${client.user.tag}`); + logger.info(`Ready! Logged in as ${client.user.tag}`); } }; diff --git a/src/lib/Errors.js b/src/lib/Errors.js new file mode 100644 index 0000000..e84f50e --- /dev/null +++ b/src/lib/Errors.js @@ -0,0 +1,12 @@ +class GeneralError extends Error { + constructor({ msg, data, code }) { + super(msg); + this.data = data; + this.code = code; + this.name = this.constructor.name; + } +} + +module.exports = { + GeneralError, +}; diff --git a/src/lib/logger.js b/src/lib/logger.js new file mode 100644 index 0000000..6017dcd --- /dev/null +++ b/src/lib/logger.js @@ -0,0 +1,27 @@ +'use strict'; +const pino = require('pino'); + +// 為了兼容vscode debugger時也能使用pino-pretty特別做的處理; pino-pretty不建議使用於prod環境 +const pinoParams = [ + { + level: 'trace', + timestamp: pino.stdTimeFunctions.isoTime, + base: undefined, + }, +]; +if (process.env.NODE_ENV === 'dev') { + const pretty = require('pino-pretty'); + const stream = pretty({ + colorize: true, + translateTime: 'SYS:mm-dd HH:MM:ss', + }); + + pinoParams.push(stream); +} + +/** + * pino document: https://github.com/pinojs/pino/blob/HEAD/docs/api.md + */ +const Logger = pino(...pinoParams); + +module.exports = Logger; diff --git a/src/routes/index.js b/src/routes/index.js deleted file mode 100644 index 72eaaae..0000000 --- a/src/routes/index.js +++ /dev/null @@ -1,87 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const { db } = require('../config/db'); -const { pageSize } = require('../const.js'); - -function validate(req, res, next) { - const { date, discordId, page } = req.query; - if (page && typeof Number(page) && Number(page) <= 0) { - return res.status(400).json('page must be valid number'); - } - - const dateRegex = /([12]\d{3}-(0[1-9]|1[0-2]))/; - if (date && !dateRegex.test(date)) { - return res.status(400).json('date must be in YYYY-MM format'); - } - - if (discordId && typeof discordId !== 'string') { - return res.status(400).json('discordId must be string'); - } - - next(); -} - -router.get('/healthCheck', (req, res) => { - return res.send('ok'); -}); - -router.get('/leaderboard', validate, async function (req, res) { - try { - const currentYearAndMonth = new Date().toISOString().slice(0, 7); - const { date, discordId, page = 1 } = req.query; - let ref = db - .collection(`leaderboard-${currentYearAndMonth}`) - .where('period', '=', date || currentYearAndMonth); - - const countSnapshot = await ref.count().get(); - const totalDocsCount = countSnapshot.data().count; - const totalPages = Math.ceil(totalDocsCount / 10); - const offset = (page - 1) * pageSize; - - if (discordId) { - ref = ref.where('discordId', '=', discordId); - } - - const querySnapshot = await ref - .orderBy('point', 'desc') - .limit(pageSize) - .offset(offset) - .get(); - - const Discord = require('discord.js'); - const client = new Discord.Client({ - intents: [] - }); - client.login(process.env.DISCORD_TOKEN); - - const fetchUserDetails = async (doc) => { - const data = doc.data(); - const guild = await client.guilds.fetch(process.env.DISCORD_GUILDID); - const member = await guild.members.fetch(data.discordId); - - return { - id: doc.id, - name: member.nickname || member.user.username, - avatarURL: member.user.displayAvatarURL(), - ...data - }; - }; - - const promises = querySnapshot.docs.map(fetchUserDetails); - const results = await Promise.all(promises); - - return res.status(200).json({ - data: results, - offset, - pageSize, - totalPages, - currentPage: page, - totalDataCount: totalDocsCount - }); - } catch (err) { - console.error(err); - return res.status(500).json(err); - } -}); - -module.exports = router; diff --git a/types/express.d.ts b/types/express.d.ts new file mode 100644 index 0000000..cc202a3 --- /dev/null +++ b/types/express.d.ts @@ -0,0 +1,9 @@ +declare namespace Express { + interface Request { + requestId: string; + } + interface Response { + fail: (error: Error) => void; + success: ({ data: object, ...info }) => void; + } +} \ No newline at end of file diff --git a/types/global.d.ts b/types/global.d.ts new file mode 100644 index 0000000..e69de29