From 48b0502cd7bec25ae4ad76fca67711ff6d01463a Mon Sep 17 00:00:00 2001 From: Mari Date: Mon, 13 Dec 2021 23:54:39 -0500 Subject: [PATCH] Add game server and pull command. --- .idea/codeStyles/codeStyleConfig.xml | 5 + package-lock.json | 907 +++++++++++++++++- package.json | 9 + src/BaseServer.ts | 92 ++ src/CookieHelpers.ts | 32 + src/DiscordWebhookHandler.ts | 90 +- src/FastifyHelpers.ts | 73 +- src/GameServer.ts | 169 ++-- src/PugRenderer.ts | 18 +- src/SavedWebhook.ts | 39 +- src/SetupServer.ts | 192 ++++ src/app.ts | 50 +- src/commands/SetupCommand.ts | 28 + src/commands/game/PullCommand.ts | 56 ++ static/pages/error.pug | 10 - static/pages/game/running.pug | 11 + static/pages/game/stop.pug | 13 + .../pages/{setupAdmin.pug => setup/admin.pug} | 2 +- static/pages/setup/done.pug | 14 + static/pages/setup/error.pug | 13 + .../pages/{setupGame.pug => setup/game.pug} | 4 +- static/pages/setup/start.pug | 13 + static/pages/setupDone.pug | 12 - static/pages/shutdown.pug | 6 + static/template/template.pug | 52 +- tsconfig.json | 2 +- 26 files changed, 1724 insertions(+), 188 deletions(-) create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 src/BaseServer.ts create mode 100644 src/CookieHelpers.ts create mode 100644 src/SetupServer.ts create mode 100644 src/commands/SetupCommand.ts create mode 100644 src/commands/game/PullCommand.ts delete mode 100644 static/pages/error.pug create mode 100644 static/pages/game/running.pug create mode 100644 static/pages/game/stop.pug rename static/pages/{setupAdmin.pug => setup/admin.pug} (91%) create mode 100644 static/pages/setup/done.pug create mode 100644 static/pages/setup/error.pug rename static/pages/{setupGame.pug => setup/game.pug} (87%) create mode 100644 static/pages/setup/start.pug delete mode 100644 static/pages/setupDone.pug create mode 100644 static/pages/shutdown.pug diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 185f76c..02f1694 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,19 +9,28 @@ "version": "0.0.1", "dependencies": { "axios": "^0.24.0", + "chance": "^1.1.8", + "crypto-random-string": "^4.0.0", "detritus-client": "^0.16.3", "detritus-client-rest": "^0.10.5", "discord-api-types": "^0.25.2", "dotenv": "^10.0.0", "fastify": "^3.24.1", + "fastify-cookie": "^5.4.0", + "pino": "^7.5.1", + "pino-discord": "^1.0.2", "pug": "^3.0.2", + "relateurl": "^0.2.7", "simple-oauth2": "^4.2.0", "slash-create": "^4.4.0" }, "devDependencies": { + "@types/chance": "^1.1.3", "@types/node": "^16.11.12", "@types/pug": "^2.0.5", + "@types/relateurl": "^0.2.29", "@types/simple-oauth2": "^4.1.1", + "pino-pretty": "^7.3.0", "typescript": "^4.5.3" } }, @@ -127,6 +136,12 @@ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" }, + "node_modules/@types/chance": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/chance/-/chance-1.1.3.tgz", + "integrity": "sha512-X6c6ghhe4/sQh4XzcZWSFaTAUOda38GQHmq9BUanYkOE/EO7ZrkazwKmtsj3xzTjkLWmwULE++23g3d3CCWaWw==", + "dev": true + }, "node_modules/@types/node": { "version": "16.11.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", @@ -147,6 +162,12 @@ "integrity": "sha512-LOnASQoeNZMkzexRuyqcBBDZ6rS+rQxUMkmj5A0PkhhiSZivLIuz6Hxyr1mkGoEZEkk66faROmpMi4fFkrKsBA==", "dev": true }, + "node_modules/@types/relateurl": { + "version": "0.2.29", + "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.29.tgz", + "integrity": "sha512-QSvevZ+IRww2ldtfv1QskYsqVVVwCKQf1XbwtcyyoRvLIQzfyPhj/C+3+PKzSDRdiyejaiLgnq//XTkleorpLg==", + "dev": true + }, "node_modules/@types/simple-oauth2": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/simple-oauth2/-/simple-oauth2-4.1.1.tgz", @@ -184,11 +205,38 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=" }, + "node_modules/args": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", + "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", + "dev": true, + "dependencies": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -254,6 +302,34 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chance": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.8.tgz", + "integrity": "sha512-v7fi5Hj2VbR6dJEGRWLmJBA83LJMS47pkAbmROFxHWd9qmE1esHRZW8Clf1Fhzr3rjxnNZVCjOEv/ivFxeIMtg==" + }, "node_modules/character-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", @@ -262,6 +338,27 @@ "is-regex": "^1.0.3" } }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", + "dev": true + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -273,6 +370,14 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "engines": { + "node": ">= 6" + } + }, "node_modules/constantinople": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", @@ -290,6 +395,37 @@ "node": ">= 0.6" } }, + "node_modules/cookie-signature": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.1.0.tgz", + "integrity": "sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/debug": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", @@ -450,6 +586,34 @@ "node": ">=10" } }, + "node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/end-of-stream": { + "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==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -519,16 +683,61 @@ "tiny-lru": "^7.0.0" } }, + "node_modules/fastify-cookie": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fastify-cookie/-/fastify-cookie-5.4.0.tgz", + "integrity": "sha512-uKTbOLx6pSyKqA9oD2G9hpMuRTVtKRm98bRwJVg4ga7GCm+RR6771stmfhbblXxHHcQQHuHvwdOdPeHJjr2sgg==", + "dependencies": { + "cookie-signature": "^1.1.0", + "fastify-plugin": "^3.0.0" + } + }, "node_modules/fastify-error": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.1.tgz", "integrity": "sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ==" }, + "node_modules/fastify-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz", + "integrity": "sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w==" + }, "node_modules/fastify-warning": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz", "integrity": "sha512-s1EQguBw/9qtc1p/WTY4eq9WMRIACkj+HTcOIK1in4MV5aFaQC9ZCIt0dJ7pr5bIf4lPpHvAtP2ywpTNgs7hqw==" }, + "node_modules/fastify/node_modules/pino": { + "version": "6.13.3", + "resolved": "https://registry.npmjs.org/pino/-/pino-6.13.3.tgz", + "integrity": "sha512-tJy6qVgkh9MwNgqX1/oYi3ehfl2Y9H0uHyEEMsBe74KinESIjdMrMQDWpcZPpPicg3VV35d/GLQZmo4QgU2Xkg==", + "dependencies": { + "fast-redact": "^3.0.0", + "fast-safe-stringify": "^2.0.8", + "fastify-warning": "^0.2.0", + "flatstr": "^1.0.12", + "pino-std-serializers": "^3.1.0", + "quick-format-unescaped": "^4.0.3", + "sonic-boom": "^1.0.2" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/fastify/node_modules/pino-std-serializers": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", + "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" + }, + "node_modules/fastify/node_modules/sonic-boom": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", + "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "flatstr": "^1.0.12" + } + }, "node_modules/fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -601,6 +810,14 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -625,6 +842,15 @@ "node": ">= 0.4.0" } }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", @@ -650,6 +876,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -710,6 +941,15 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", @@ -729,6 +969,15 @@ "promise": "^7.0.1" } }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/light-my-request": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-4.7.0.tgz", @@ -800,6 +1049,15 @@ "node": ">= 0.6" } }, + "node_modules/mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -824,32 +1082,103 @@ "node": ">=0.10.0" } }, + "node_modules/on-exit-leak-free": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", + "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/pino": { - "version": "6.13.3", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.13.3.tgz", - "integrity": "sha512-tJy6qVgkh9MwNgqX1/oYi3ehfl2Y9H0uHyEEMsBe74KinESIjdMrMQDWpcZPpPicg3VV35d/GLQZmo4QgU2Xkg==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-7.5.1.tgz", + "integrity": "sha512-Wzo2G7CLaRHKOz3+Ex006LC5Xi0xEUm+mwm/h0NKzuKZONdekcbmjXg7vWDoO8hVTGX+1RuUy2fwlzvZ24EI5A==", "dependencies": { "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.8", "fastify-warning": "^0.2.0", - "flatstr": "^1.0.12", - "pino-std-serializers": "^3.1.0", + "get-caller-file": "^2.0.5", + "on-exit-leak-free": "^0.2.0", + "pino-abstract-transport": "v0.5.0", + "pino-std-serializers": "^4.0.0", "quick-format-unescaped": "^4.0.3", - "sonic-boom": "^1.0.2" + "real-require": "^0.1.0", + "safe-stable-stringify": "^2.1.0", + "sonic-boom": "^2.2.1", + "thread-stream": "^0.13.0" }, "bin": { "pino": "bin.js" } }, + "node_modules/pino-abstract-transport": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", + "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", + "dependencies": { + "duplexify": "^4.1.2", + "split2": "^4.0.0" + } + }, + "node_modules/pino-discord": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pino-discord/-/pino-discord-1.0.2.tgz", + "integrity": "sha512-BpFTPwnNrqNB9YvXaMrylq6264KZ/OswflpNGeAOBGme8sdKR94uxlg+mADTDvoUW+s98r+XN3NPeDjKGTwSLg==", + "dependencies": { + "commander": "^6.1.0", + "pump": "^3.0.0", + "split2": "^3.2.2", + "through2": "^4.0.2" + }, + "bin": { + "pino-discord": "index.js" + } + }, + "node_modules/pino-discord/node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-7.3.0.tgz", + "integrity": "sha512-HAhShJ2z2QzxXhYAn6XfwYpF13o1PQbjzSNA9q+30FAvhjOmeACit9lprhV/mCOw/8YFWSyyNk0YCq2EDYGYpw==", + "dev": true, + "dependencies": { + "args": "^5.0.1", + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-safe-stringify": "^2.0.7", + "joycon": "^3.1.1", + "pino-abstract-transport": "^0.5.0", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^2.2.0", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, "node_modules/pino-std-serializers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", - "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", + "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==" }, "node_modules/promise": { "version": "7.3.1", @@ -983,6 +1312,15 @@ "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" }, + "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.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -1015,6 +1353,35 @@ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", + "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/require-all": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/require-all/-/require-all-3.0.0.tgz", @@ -1065,6 +1432,25 @@ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex2": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", @@ -1073,6 +1459,14 @@ "ret": "~0.2.0" } }, + "node_modules/safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==", + "engines": { + "node": ">=10" + } + }, "node_modules/secure-json-parse": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.4.0.tgz", @@ -1142,12 +1536,32 @@ } }, "node_modules/sonic-boom": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", - "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.4.1.tgz", + "integrity": "sha512-WgtVLfGl347/zS1oTuLaOAvVD5zijgjphAJHgbbnBJGgexnr+C1ULSj0j7ftoGxpuxR4PaV635NkwFemG8m/5w==", "dependencies": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", + "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" } }, "node_modules/string-similarity": { @@ -1155,6 +1569,46 @@ "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz", "integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==" }, + "node_modules/strip-json-comments": { + "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==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/thread-stream": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.13.0.tgz", + "integrity": "sha512-kTMZeX4Dzlb1zZ00/01aerGaTw2i8NE4sWF0TvF1uXewRhCiUjCvatQkvxIvFqauWG2ADFS2Wpd3qBeYL9i3dg==", + "dependencies": { + "real-require": "^0.1.0" + } + }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dependencies": { + "readable-stream": "3" + } + }, "node_modules/tiny-lru": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-7.0.6.tgz", @@ -1186,6 +1640,17 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" }, + "node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.3.tgz", @@ -1207,6 +1672,11 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -1243,6 +1713,11 @@ "node": ">= 10.0.0" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, "node_modules/ws": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", @@ -1356,6 +1831,12 @@ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" }, + "@types/chance": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/chance/-/chance-1.1.3.tgz", + "integrity": "sha512-X6c6ghhe4/sQh4XzcZWSFaTAUOda38GQHmq9BUanYkOE/EO7ZrkazwKmtsj3xzTjkLWmwULE++23g3d3CCWaWw==", + "dev": true + }, "@types/node": { "version": "16.11.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.12.tgz", @@ -1376,6 +1857,12 @@ "integrity": "sha512-LOnASQoeNZMkzexRuyqcBBDZ6rS+rQxUMkmj5A0PkhhiSZivLIuz6Hxyr1mkGoEZEkk66faROmpMi4fFkrKsBA==", "dev": true }, + "@types/relateurl": { + "version": "0.2.29", + "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.29.tgz", + "integrity": "sha512-QSvevZ+IRww2ldtfv1QskYsqVVVwCKQf1XbwtcyyoRvLIQzfyPhj/C+3+PKzSDRdiyejaiLgnq//XTkleorpLg==", + "dev": true + }, "@types/simple-oauth2": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/simple-oauth2/-/simple-oauth2-4.1.1.tgz", @@ -1403,11 +1890,32 @@ "uri-js": "^4.2.2" } }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, "archy": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=" }, + "args": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", + "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", + "dev": true, + "requires": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + } + }, "asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -1464,6 +1972,28 @@ "get-intrinsic": "^1.0.2" } }, + "camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chance": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.8.tgz", + "integrity": "sha512-v7fi5Hj2VbR6dJEGRWLmJBA83LJMS47pkAbmROFxHWd9qmE1esHRZW8Clf1Fhzr3rjxnNZVCjOEv/ivFxeIMtg==" + }, "character-parser": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", @@ -1472,6 +2002,27 @@ "is-regex": "^1.0.3" } }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "colorette": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", + "dev": true + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1480,6 +2031,11 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" + }, "constantinople": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", @@ -1494,6 +2050,25 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" }, + "cookie-signature": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.1.0.tgz", + "integrity": "sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A==" + }, + "crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "requires": { + "type-fest": "^1.0.1" + } + }, + "dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true + }, "debug": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", @@ -1589,6 +2164,31 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" }, + "duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "end-of-stream": { + "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==", + "requires": { + "once": "^1.4.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, "eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -1650,6 +2250,45 @@ "secure-json-parse": "^2.0.0", "semver": "^7.3.2", "tiny-lru": "^7.0.0" + }, + "dependencies": { + "pino": { + "version": "6.13.3", + "resolved": "https://registry.npmjs.org/pino/-/pino-6.13.3.tgz", + "integrity": "sha512-tJy6qVgkh9MwNgqX1/oYi3ehfl2Y9H0uHyEEMsBe74KinESIjdMrMQDWpcZPpPicg3VV35d/GLQZmo4QgU2Xkg==", + "requires": { + "fast-redact": "^3.0.0", + "fast-safe-stringify": "^2.0.8", + "fastify-warning": "^0.2.0", + "flatstr": "^1.0.12", + "pino-std-serializers": "^3.1.0", + "quick-format-unescaped": "^4.0.3", + "sonic-boom": "^1.0.2" + } + }, + "pino-std-serializers": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", + "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" + }, + "sonic-boom": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", + "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "requires": { + "atomic-sleep": "^1.0.0", + "flatstr": "^1.0.12" + } + } + } + }, + "fastify-cookie": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fastify-cookie/-/fastify-cookie-5.4.0.tgz", + "integrity": "sha512-uKTbOLx6pSyKqA9oD2G9hpMuRTVtKRm98bRwJVg4ga7GCm+RR6771stmfhbblXxHHcQQHuHvwdOdPeHJjr2sgg==", + "requires": { + "cookie-signature": "^1.1.0", + "fastify-plugin": "^3.0.0" } }, "fastify-error": { @@ -1657,6 +2296,11 @@ "resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.1.tgz", "integrity": "sha512-oCfpcsDndgnDVgiI7bwFKAun2dO+4h84vBlkWsWnz/OUK9Reff5UFoFl241xTiLeHWX/vU9zkDVXqYUxjOwHcQ==" }, + "fastify-plugin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-3.0.0.tgz", + "integrity": "sha512-ZdCvKEEd92DNLps5n0v231Bha8bkz1DjnPP/aEz37rz/q42Z5JVLmgnqR4DYuNn3NXAO3IDCPyRvgvxtJ4Ym4w==" + }, "fastify-warning": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/fastify-warning/-/fastify-warning-0.2.0.tgz", @@ -1711,6 +2355,11 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, "get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -1729,6 +2378,12 @@ "function-bind": "^1.1.1" } }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, "has-symbols": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", @@ -1742,6 +2397,11 @@ "has-symbols": "^1.0.2" } }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1790,6 +2450,12 @@ "@sideway/pinpoint": "^2.0.0" } }, + "joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true + }, "js-stringify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", @@ -1809,6 +2475,12 @@ "promise": "^7.0.1" } }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true + }, "light-my-request": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-4.7.0.tgz", @@ -1869,6 +2541,12 @@ "mime-db": "1.51.0" } }, + "mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -1887,29 +2565,96 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, + "on-exit-leak-free": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", + "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, "path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "pino": { - "version": "6.13.3", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.13.3.tgz", - "integrity": "sha512-tJy6qVgkh9MwNgqX1/oYi3ehfl2Y9H0uHyEEMsBe74KinESIjdMrMQDWpcZPpPicg3VV35d/GLQZmo4QgU2Xkg==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-7.5.1.tgz", + "integrity": "sha512-Wzo2G7CLaRHKOz3+Ex006LC5Xi0xEUm+mwm/h0NKzuKZONdekcbmjXg7vWDoO8hVTGX+1RuUy2fwlzvZ24EI5A==", "requires": { "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.8", "fastify-warning": "^0.2.0", - "flatstr": "^1.0.12", - "pino-std-serializers": "^3.1.0", + "get-caller-file": "^2.0.5", + "on-exit-leak-free": "^0.2.0", + "pino-abstract-transport": "v0.5.0", + "pino-std-serializers": "^4.0.0", "quick-format-unescaped": "^4.0.3", - "sonic-boom": "^1.0.2" + "real-require": "^0.1.0", + "safe-stable-stringify": "^2.1.0", + "sonic-boom": "^2.2.1", + "thread-stream": "^0.13.0" + } + }, + "pino-abstract-transport": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", + "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", + "requires": { + "duplexify": "^4.1.2", + "split2": "^4.0.0" + } + }, + "pino-discord": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pino-discord/-/pino-discord-1.0.2.tgz", + "integrity": "sha512-BpFTPwnNrqNB9YvXaMrylq6264KZ/OswflpNGeAOBGme8sdKR94uxlg+mADTDvoUW+s98r+XN3NPeDjKGTwSLg==", + "requires": { + "commander": "^6.1.0", + "pump": "^3.0.0", + "split2": "^3.2.2", + "through2": "^4.0.2" + }, + "dependencies": { + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "requires": { + "readable-stream": "^3.0.0" + } + } + } + }, + "pino-pretty": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-7.3.0.tgz", + "integrity": "sha512-HAhShJ2z2QzxXhYAn6XfwYpF13o1PQbjzSNA9q+30FAvhjOmeACit9lprhV/mCOw/8YFWSyyNk0YCq2EDYGYpw==", + "dev": true, + "requires": { + "args": "^5.0.1", + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-safe-stringify": "^2.0.7", + "joycon": "^3.1.1", + "pino-abstract-transport": "^0.5.0", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^2.2.0", + "strip-json-comments": "^3.1.1" } }, "pino-std-serializers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", - "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", + "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==" }, "promise": { "version": "7.3.1", @@ -2040,6 +2785,15 @@ "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==" }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -2055,6 +2809,26 @@ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "real-require": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.1.0.tgz", + "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==" + }, + "relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=" + }, "require-all": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/require-all/-/require-all-3.0.0.tgz", @@ -2089,6 +2863,11 @@ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, "safe-regex2": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", @@ -2097,6 +2876,11 @@ "ret": "~0.2.0" } }, + "safe-stable-stringify": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz", + "integrity": "sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==" + }, "secure-json-parse": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.4.0.tgz", @@ -2145,12 +2929,29 @@ } }, "sonic-boom": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", - "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-2.4.1.tgz", + "integrity": "sha512-WgtVLfGl347/zS1oTuLaOAvVD5zijgjphAJHgbbnBJGgexnr+C1ULSj0j7ftoGxpuxR4PaV635NkwFemG8m/5w==", "requires": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" + "atomic-sleep": "^1.0.0" + } + }, + "split2": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz", + "integrity": "sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==" + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" } }, "string-similarity": { @@ -2158,6 +2959,37 @@ "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz", "integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==" }, + "strip-json-comments": { + "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==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "thread-stream": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.13.0.tgz", + "integrity": "sha512-kTMZeX4Dzlb1zZ00/01aerGaTw2i8NE4sWF0TvF1uXewRhCiUjCvatQkvxIvFqauWG2ADFS2Wpd3qBeYL9i3dg==", + "requires": { + "real-require": "^0.1.0" + } + }, + "through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "requires": { + "readable-stream": "3" + } + }, "tiny-lru": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-7.0.6.tgz", @@ -2183,6 +3015,11 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" }, + "type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==" + }, "typescript": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.3.tgz", @@ -2197,6 +3034,11 @@ "punycode": "^2.1.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, "void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -2227,6 +3069,11 @@ "babel-walk": "3.0.0-canary-5" } }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, "ws": { "version": "7.5.6", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz", diff --git a/package.json b/package.json index 9994500..8e99f6e 100644 --- a/package.json +++ b/package.json @@ -3,19 +3,28 @@ "version": "0.0.1", "dependencies": { "axios": "^0.24.0", + "chance": "^1.1.8", + "crypto-random-string": "^4.0.0", "detritus-client": "^0.16.3", "detritus-client-rest": "^0.10.5", "discord-api-types": "^0.25.2", "dotenv": "^10.0.0", "fastify": "^3.24.1", + "fastify-cookie": "^5.4.0", + "pino": "^7.5.1", + "pino-discord": "^1.0.2", "pug": "^3.0.2", + "relateurl": "^0.2.7", "simple-oauth2": "^4.2.0", "slash-create": "^4.4.0" }, "devDependencies": { + "@types/chance": "^1.1.3", "@types/node": "^16.11.12", "@types/pug": "^2.0.5", + "@types/relateurl": "^0.2.29", "@types/simple-oauth2": "^4.1.1", + "pino-pretty": "^7.3.0", "typescript": "^4.5.3" }, "type": "module", diff --git a/src/BaseServer.ts b/src/BaseServer.ts new file mode 100644 index 0000000..130cf72 --- /dev/null +++ b/src/BaseServer.ts @@ -0,0 +1,92 @@ +import {SavedWebhook} from "./SavedWebhook.js"; +import fastify, {FastifyInstance} from "fastify"; +import {SlashCreator, SlashCreatorOptions} from "slash-create"; +import fastifyCookie from "fastify-cookie"; +import {FastifyServerButItWorksUnlikeTheRealOne} from "./FastifyHelpers.js"; + +export interface BaseServerDeps { + appId: string + botToken: string + clientSecret: string + publicKey: string + listenPort: number + listenAddress: string + cookieSecret: string + gameWebhook: SavedWebhook + adminWebhook: SavedWebhook +} + +export class BaseServer { + readonly server: FastifyInstance + readonly gameWebhook: SavedWebhook + readonly adminWebhook: SavedWebhook + readonly appId: string + readonly clientSecret: string + readonly listenPort: number + readonly listenAddress: string + readonly slashcmd: SlashCreator + + constructor({ + cookieSecret, + appId, + clientSecret, + listenPort, + listenAddress, + gameWebhook, + adminWebhook, + publicKey, + botToken, + slashCreatorOptions = {} + }: BaseServerDeps & { slashCreatorOptions?: Partial }) { + this.slashcmd = new SlashCreator({ + allowedMentions: {everyone: false, roles: false, users: false}, + applicationID: appId, + defaultImageFormat: "webp", + handleCommandsManually: false, + maxSignatureTimestamp: 3000, + publicKey, + requestTimeout: 10000, + token: botToken, + unknownCommandResponse: true, + endpointPath: "/interactions", + ...slashCreatorOptions, + }) + this.server = fastify({logger: true}) + this.slashcmd.withServer(new FastifyServerButItWorksUnlikeTheRealOne(this.server, {alreadyListening: true})) + this.server.register(fastifyCookie, { + secret: cookieSecret, + }) + this.appId = appId + this.clientSecret = clientSecret + this.listenPort = listenPort + this.listenAddress = listenAddress + this.gameWebhook = gameWebhook + this.adminWebhook = adminWebhook + + } + + async _initInternal(): Promise { + return + } + + async initialize(): Promise { + await this._initInternal() + await this.slashcmd.syncCommandsAsync({ + syncGuilds: true, + syncPermissions: true, + skipGuildErrors: false, + deleteCommands: true, + }) + await this.slashcmd.startServer() + await this.server.listen(this.listenPort, this.listenAddress) + } + + async _shutdownInternal(): Promise { + return + } + + async shutdown(): Promise { + await this._shutdownInternal() + return this.server.close() + } +} \ No newline at end of file diff --git a/src/CookieHelpers.ts b/src/CookieHelpers.ts new file mode 100644 index 0000000..d2d62ff --- /dev/null +++ b/src/CookieHelpers.ts @@ -0,0 +1,32 @@ +import {FastifyReply, FastifyRequest} from "fastify"; +import {RouteGenericInterface} from "fastify/types/route"; +import cryptoRandomString from "crypto-random-string"; + +export interface XSRFRoute extends RouteGenericInterface { + Querystring: { [key in typeof XSRFParameter]: string | string[] | undefined } +} + +export const XSRFCookie = "__Host-XSRF-Cookie"; +export const XSRFParameter = "state" as const; + +export function generateXSRFCookie(res: FastifyReply): string { + const newState = cryptoRandomString({ + length: 32, + type: 'url-safe' + }) + res.setCookie(XSRFCookie, newState, { + path: "/", + sameSite: "strict", + httpOnly: true, + signed: true, + secure: true, + }) + return newState +} + +export function checkAndClearXSRFCookie(req: FastifyRequest, res: FastifyReply): boolean { + const queryState = req.query[XSRFParameter] ?? null + const cookieState = req.cookies[XSRFCookie] ?? null + res.clearCookie(XSRFCookie) + return cookieState !== null && queryState !== null && cookieState === queryState +} \ No newline at end of file diff --git a/src/DiscordWebhookHandler.ts b/src/DiscordWebhookHandler.ts index 569cb0d..ec804fc 100644 --- a/src/DiscordWebhookHandler.ts +++ b/src/DiscordWebhookHandler.ts @@ -4,11 +4,13 @@ import {AccessToken, AuthorizationCode, ModuleOptions} from "simple-oauth2"; import pug from "pug"; import {renderError} from "./PugRenderer.js"; import {APIWebhook} from "discord-api-types"; -import {getBaseUrl, getFirstValue, RouteWithQuerystring} from "./FastifyHelpers.js"; -import { join } from "path"; +import {getBaseUrl, getFirstValue} from "./FastifyHelpers.js"; +import {join, relative} from "path"; +import {RouteGenericInterface} from "fastify/types/route"; +import {checkAndClearXSRFCookie, generateXSRFCookie} from "./CookieHelpers.js"; interface DiscordWebhookToken extends AccessToken { - token: {webhook?: APIWebhook} + token: { webhook?: APIWebhook } } const AuthConfig: ModuleOptions["auth"] = { @@ -18,12 +20,29 @@ const AuthConfig: ModuleOptions["auth"] = { revokePath: "/api/oauth2/token/revoke", } +export interface OAuthRoute extends RouteGenericInterface { + Querystring: { + code: string | string[] | undefined, + state: string | string[] | undefined, + error: string | string[] | undefined, + error_description: string | string[] | undefined, + error_uri: string | string[] | undefined, + } +} + export class DiscordWebhookHandler { readonly webhook: SavedWebhook readonly templateFilename: string readonly config: ModuleOptions readonly destinationFunc: () => string - constructor({webhook, templateFilename, appId, secret, destinationFunc}: {webhook: SavedWebhook, templateFilename: string, appId: string, secret: string, destinationFunc: () => string}) { + + constructor({ + webhook, + templateFilename, + appId, + secret, + destinationFunc + }: { webhook: SavedWebhook, templateFilename: string, appId: string, secret: string, destinationFunc: () => string }) { this.templateFilename = templateFilename this.webhook = webhook this.config = { @@ -35,20 +54,56 @@ export class DiscordWebhookHandler { } this.destinationFunc = destinationFunc } - async handleRequest(req: FastifyRequest, res: FastifyReply): Promise { + + async handleRequest(req: FastifyRequest, res: FastifyReply): Promise { const baseUrl = getBaseUrl(req) - const code = getFirstValue(req.query["code"]) ?? null + const withoutParams = new URL(req.url) + withoutParams.search = "" + const code = getFirstValue(req.query.code) ?? null + const errorCode = getFirstValue(req.query.error) ?? null const client = new AuthorizationCode(this.config) - if (code === null) { + if (code === null && errorCode === null) { + const state = generateXSRFCookie(res) const authUrl = client.authorizeURL({ scope: ["applications.commands", "webhook.incoming"], - redirect_uri: req.url, + redirect_uri: withoutParams.toString(), + state, }) res.code(200) res.type("text/html") - res.send(pug.renderFile(join("static/pages", this.templateFilename), {authUrl, baseUrl, isReset: this.webhook.isPresent})) + res.send(pug.renderFile(join("static/pages", this.templateFilename), { + authUrl, + baseUrl, + isReset: this.webhook.isPresent + })) return } + const validXSRF = checkAndClearXSRFCookie(req, res) + if (errorCode !== null || code === null) { + const errorDescription = getFirstValue(req.query.error_description) ?? null + const errorUrl = getFirstValue(req.query.error_uri) + return renderError({ + baseUrl, + res, + code: 400, + error: errorDescription ?? errorCode ?? "There was no code or error present.", + context: "authorizing the application", + errorUrl, + buttonText: "Try Again", + buttonUrl: relative("/", new URL(req.url).pathname), + }) + } + if (!validXSRF) { + return renderError({ + baseUrl, + res, + code: 409, + error: "The state was incorrectly set - try restarting the authentication process.", + context: "processing the new access code", + buttonText: "Try Again", + buttonUrl: relative("/", new URL(req.url).pathname), + }) + } let token: DiscordWebhookToken try { token = await client.getToken({ @@ -61,15 +116,21 @@ export class DiscordWebhookHandler { baseUrl, res: res, error: e, - context: "exchanging the code you gave me for a token" + code: 400, + context: "exchanging the code you gave me for a token", + buttonText: "Try Again", + buttonUrl: relative("/", new URL(req.url).pathname), }) } if (!token.token.webhook) { return renderError({ baseUrl, res: res, - error: "token did not contain webhook", - context: "processing the token I received" + code: 400, + error: "The token did not contain a webhook.", + context: "processing the token I received", + buttonText: "Try Again", + buttonUrl: relative("/", new URL(req.url).pathname), }) } const wasSet = this.webhook.isPresent @@ -79,8 +140,11 @@ export class DiscordWebhookHandler { return renderError({ baseUrl, res: res, + code: 500, error: e, - context: `saving the new webhook${wasSet ? " and deleting the old one" : ""}` + context: `saving the new webhook${wasSet ? " and deleting the old one" : ""}`, + buttonText: "Try Again", + buttonUrl: relative("/", new URL(req.url).pathname), }) } res.redirect(this.destinationFunc()) diff --git a/src/FastifyHelpers.ts b/src/FastifyHelpers.ts index 48c531d..c25072d 100644 --- a/src/FastifyHelpers.ts +++ b/src/FastifyHelpers.ts @@ -1,10 +1,6 @@ /** Constructs the base URL from the headers from the given request. */ -import {FastifyRequest} from "fastify"; -import {RouteGenericInterface} from "fastify/types/route"; - -export interface RouteWithQuerystring extends RouteGenericInterface { - Querystring: {[key: string]: string|string[]|undefined} -} +import fastify, {FastifyInstance, FastifyRequest} from "fastify"; +import {Server, ServerOptions, ServerRequestHandler} from "slash-create"; export function getBaseUrl(request: FastifyRequest): string { const hostHeader = request.hostname ?? "localhost" @@ -14,6 +10,69 @@ export function getBaseUrl(request: FastifyRequest): string { } /** Translates a zero-to-many set of strings to one or zero strings. */ -export function getFirstValue(value: string|string[]|undefined): string|undefined { +export function getFirstValue(value: string | string[] | undefined): string | undefined { return typeof value === "string" ? value : Array.isArray(value) ? value[0] : undefined +} + +/** + * A server for Fastify applications. + * @see https://fastify.io + */ +export class FastifyServerButItWorksUnlikeTheRealOne extends Server { + readonly app: FastifyInstance; + + /** + * @param app The fastify application + * @param opts The server options + */ + constructor(app?: FastifyInstance, opts?: ServerOptions) { + super(opts); + this.app = app || fastify(); + } + + /** + * Adds middleware to the Fastify server. + * This requires you to have the 'middie' module registered to the server before using. + * @param middleware The middleware to add. + * @see https://www.fastify.io/docs/latest/Middleware/ + */ + addMiddleware(middleware: Function) { + // @ts-ignore + if ('use' in this.app) this.app.use(middleware); + else + throw new Error( + "In order to use Express-like middleware, you must initialize the server and register the 'middie' module." + ); + return this; + } + + /** Alias for {@link FastifyServerButItWorksUnlikeTheRealOne#addMiddleware} */ + use(middleware: Function) { + return this.addMiddleware(middleware); + } + + /** @private */ + createEndpoint(path: string, handler: ServerRequestHandler) { + this.app.post(path, (req: any, res: any) => + handler( + { + headers: req.headers, + body: req.body, + request: req, + response: res + }, + async (response) => { + res.status(response.status || 200); + if (response.headers) res.headers(response.headers); + res.send(response.body); + } + ) + ); + } + + /** @private */ + async listen(port = 8030, host = 'localhost') { + if (this.alreadyListening) return; + await this.app.listen(port, host); + } } \ No newline at end of file diff --git a/src/GameServer.ts b/src/GameServer.ts index bc2d811..feccd17 100644 --- a/src/GameServer.ts +++ b/src/GameServer.ts @@ -1,100 +1,109 @@ -import {SavedWebhook} from "./SavedWebhook.js"; -import fastify, {FastifyInstance} from "fastify"; -import {DiscordWebhookHandler} from "./DiscordWebhookHandler.js"; -import {getBaseUrl, RouteWithQuerystring} from "./FastifyHelpers.js"; -import pug from "pug"; +import {SetupServer} from "./SetupServer.js"; +import {checkAndClearXSRFCookie, generateXSRFCookie, XSRFRoute} from "./CookieHelpers.js"; import {renderError} from "./PugRenderer.js"; +import {getBaseUrl} from "./FastifyHelpers.js"; +import pug from "pug"; +import {BaseServer, BaseServerDeps} from "./BaseServer.js"; +import {PullCommand} from "./commands/game/PullCommand.js"; -export class GameServer { - readonly server: FastifyInstance - readonly gameWebhook: SavedWebhook - readonly adminWebhook: SavedWebhook - readonly appId: string - readonly secret: string - readonly port: number - constructor({appId, secret, port}: {appId: string, secret: string, port: number}) { - this.server = fastify({ - logger: true - }) - this.appId = appId - this.secret = secret - this.port = port - this.gameWebhook = new SavedWebhook("broadcasthook.json", {logger: this.server.log}) - this.adminWebhook = new SavedWebhook("logginghook.json", {logger: this.server.log}) +export class GameServer extends BaseServer { + readonly setupFactory: () => SetupServer + + constructor(deps: BaseServerDeps & { setupFactory: () => SetupServer }) { + super(deps) + this.setupFactory = deps.setupFactory } - async initialize(): Promise { - const gameHandler = new DiscordWebhookHandler({ - webhook: this.gameWebhook, - templateFilename: "setupGame.pug", - appId: this.appId, - secret: this.secret, - destinationFunc: () => { - if (this.adminWebhook.isPresent) { - return "clear" - } else { - return "adminChannel" - } - }, + async _initInternal(): Promise { + this.slashcmd.registerCommand(new PullCommand(this.slashcmd, this.gameWebhook)) + this.server.get("/game/started", async (req, res) => { + const token = generateXSRFCookie(res) + res.code(200) + res.type("text/html") + res.send(pug.renderFile("static/pages/game/running.pug", { + baseUrl: getBaseUrl(req), + setupModeUrl: `game/stop?token=${token}`, + shutdownUrl: `shutdown?token=${token}`, + })) }) - const adminHandler = new DiscordWebhookHandler({ - webhook: this.adminWebhook, - templateFilename: "setupAdmin.pug", - appId: this.appId, - secret: this.secret, - destinationFunc: () => { - if (this.gameWebhook.isPresent) { - return "clear" - } else { - return "gameChannel" - } - }, + this.server.get("/game", async (req, res) => { + res.redirect("game/started") }) - this.server.get("/setup", async (req, res) => { - if (!this.gameWebhook.isPresent) { - res.redirect(`setup/gameChannel`) - } else if (!this.adminWebhook.isPresent) { - res.redirect(`setup/adminChannel`) - } else { - res.redirect(`setup/done`) - } + this.server.get("/game/start", async (req, res) => { + res.redirect("started") + }) + this.server.get("/setup", async (req, res) => { + res.redirect("game/started") + }) + this.server.get("/setup/gameChannel", async (req, res) => { + res.redirect("../game/started") }) - this.server.get("/setup/gameChannel", async (req, res) => { - return await gameHandler.handleRequest(req, res) + this.server.get("/setup/adminChannel", async (req, res) => { + res.redirect("../game/started") }) - this.server.get("/setup/adminChannel", async (req, res) => { - return await adminHandler.handleRequest(req, res) + this.server.get("/setup/clear", async (req, res) => { + res.redirect("../game/started") }) - this.server.get("/setup/clear", async (req, res) => { - try { - await Promise.all([this.gameWebhook, this.adminWebhook] - .filter((item) => item.isPresent) - .map((item) => item.clearHook())) - } catch (e) { + this.server.get("/setup/done", async (req, res) => { + res.redirect("../game/started") + }) + this.server.get("/game/stop", async (req, res) => { + if (!checkAndClearXSRFCookie(req, res)) { return renderError({ baseUrl: getBaseUrl(req), res, - error: e, - context: "clearing the broadcast webhook" + code: 400, + error: "Token was incorrect or not set.", + context: "stopping the game", + buttonText: "Return to Game", + buttonUrl: "game/started" }) } - res.redirect(".") + res.code(200) + res.type("text/html") + res.send(pug.renderFile("static/pages/game/stop.pug", { + startedUrl: "setup" + })) + setImmediate(async () => { + this.server.log.info("Shutting down the game server and switching to the setup server.") + try { + await this.server.close() + } catch (e) { + this.server.log.error(e, "Failed to shut down the game server") + } + try { + await this.setupFactory().initialize() + } catch (e) { + this.server.log.error(e, "Failed to start up the setup server") + } + this.server.log.info("Successfully switched from the game server to the setup server.") + }) + }) - this.server.get("/setup/done", async (req, res) => { - if (!this.gameWebhook.isPresent) { - res.redirect("gameChannel") - } else if (!this.adminWebhook.isPresent) { - res.redirect("adminChannel") - } else { - res.code(200) - res.type("text/html") - res.send(pug.renderFile("static/pages/setupDone.pug", { + this.server.get("/shutdown", async (req, res) => { + if (!checkAndClearXSRFCookie(req, res)) { + return renderError({ baseUrl: getBaseUrl(req), - clearUrl: "setup/clear", - gameSetupUrl: "setup/gameChannel", - adminSetupUrl: "setup/adminChannel"})) + res, + code: 400, + error: "Token was incorrect or not set.", + context: "shutting down the game", + buttonText: "Return to Game", + buttonUrl: "game/started" + }) } + res.code(200) + res.type("text/html") + res.send(pug.renderFile("static/pages/shutdown.pug")) + setImmediate(async () => { + this.server.log.info("Shutting down the game server.") + try { + await this.server.close() + } catch (e) { + this.server.log.error(e, "Failed to shut down the game server") + } + this.server.log.info("Shut down. Good night...") + }) }) - await Promise.all([this.gameWebhook.load(), this.adminWebhook.load(), this.server.listen(this.port, "127.0.0.1")]) } } \ No newline at end of file diff --git a/src/PugRenderer.ts b/src/PugRenderer.ts index b783a96..00b46d8 100644 --- a/src/PugRenderer.ts +++ b/src/PugRenderer.ts @@ -2,12 +2,24 @@ import {FastifyReply} from "fastify"; import {renderFile} from "pug"; /** Renders the error page into the given reply. */ -export function renderError({baseUrl, res, error, context}: {baseUrl: string, res: FastifyReply, error: unknown, context: string}): void { - res.code(500) +export function renderError({ + baseUrl, + code = 500, + res, + error, + context, + errorUrl, + buttonText, + buttonUrl + }: { baseUrl: string, code?: number, res: FastifyReply, error: unknown, context: string, errorUrl?: string, buttonText?: string, buttonUrl?: string }): void { + res.code(code) res.type("text/html") - res.send(renderFile("static/pages/error.pug", { + res.send(renderFile("static/pages/setup/error.pug", { baseUrl, error, context, + errorUrl, + buttonText: buttonText ?? "Return to Setup", + buttonUrl: buttonUrl ?? "setup" })) } \ No newline at end of file diff --git a/src/SavedWebhook.ts b/src/SavedWebhook.ts index dfa9012..aa28bec 100644 --- a/src/SavedWebhook.ts +++ b/src/SavedWebhook.ts @@ -1,7 +1,7 @@ /** */ import {APIWebhook} from "discord-api-types"; import {FastifyLoggerInstance} from "fastify"; -import {readFile as fsReadFile, writeFile as fsWriteFile, mkdir as fsMkdir, rm as fsRm} from "fs/promises"; +import {mkdir as fsMkdir, readFile as fsReadFile, rm as fsRm, writeFile as fsWriteFile} from "fs/promises"; import axios from "axios"; import {join} from "path"; import {isENOENT} from "./NodeErrorHelpers.js"; @@ -11,8 +11,7 @@ const WebhookPath = "runtime/webhooks" /** File-based storage for webhook instances. */ export class SavedWebhook { readonly filename: string - private _state: APIWebhook|null|undefined - private readonly logger: FastifyLoggerInstance|null + private readonly logger: FastifyLoggerInstance | null private readonly readFile: typeof fsReadFile private readonly writeFile: typeof fsWriteFile private readonly mkdir: typeof fsMkdir @@ -20,7 +19,15 @@ export class SavedWebhook { private readonly delRequest: typeof axios["delete"] /** Initializes a SavedWebhook pointing at the given location. */ - constructor(filename: string, {logger, state, readFile, writeFile, mkdir, rm, delRequest}: {logger?: FastifyLoggerInstance|null, state?: APIWebhook|null, readFile?: typeof fsReadFile, writeFile?: typeof fsWriteFile, mkdir?: typeof fsMkdir, rm?: typeof fsRm, delRequest?: typeof axios["delete"]}) { + constructor(filename: string, { + logger, + state, + readFile, + writeFile, + mkdir, + rm, + delRequest + }: { logger?: FastifyLoggerInstance | null, state?: APIWebhook | null, readFile?: typeof fsReadFile, writeFile?: typeof fsWriteFile, mkdir?: typeof fsMkdir, rm?: typeof fsRm, delRequest?: typeof axios["delete"] }) { this.filename = filename this._state = state this.logger = logger?.child({webhookFile: filename}) ?? null @@ -31,6 +38,17 @@ export class SavedWebhook { this.delRequest = delRequest ?? axios.delete.bind(axios) } + private _state: APIWebhook | null | undefined + + /** Gets the current state of the webhook, either a webhook instance or null. */ + get state(): APIWebhook | null { + if (this._state === undefined) { + this.logger?.warn("SavedWebhook was not initialized before having its state checked") + return null + } + return this._state + } + /** The path of the webhook, including WebhookPath. */ get path(): string { return join(WebhookPath, this.filename) @@ -41,17 +59,8 @@ export class SavedWebhook { return this.state !== null } - /** Gets the current state of the webhook, either a webhook instance or null. */ - get state(): APIWebhook|null { - if (this._state === undefined) { - this.logger?.warn("SavedWebhook was not initialized before having its state checked") - return null - } - return this._state - } - /** Loads the current state from the disk, including a null state if not present. */ - async load(): Promise { + async load(): Promise { if (this._state !== undefined) { this.logger?.warn(`SavedWebhook was double-initialized`) } @@ -138,6 +147,6 @@ export class SavedWebhook { return } await this.delRequest(`https://discord.com/api/webhooks/${this._state.id}/${this._state.token}`, - {validateStatus: (s) => s === 204 || s === 404 }) + {validateStatus: (s) => s === 204 || s === 404}) } } \ No newline at end of file diff --git a/src/SetupServer.ts b/src/SetupServer.ts new file mode 100644 index 0000000..ca1497e --- /dev/null +++ b/src/SetupServer.ts @@ -0,0 +1,192 @@ +import {DiscordWebhookHandler, OAuthRoute} from "./DiscordWebhookHandler.js"; +import {getBaseUrl} from "./FastifyHelpers.js"; +import pug from "pug"; +import {renderError} from "./PugRenderer.js"; +import {GameServer} from "./GameServer.js"; +import {checkAndClearXSRFCookie, generateXSRFCookie, XSRFRoute} from "./CookieHelpers.js"; +import {SetupCommand, setupComponentInteractionHandler} from "./commands/SetupCommand.js"; +import {BaseServer, BaseServerDeps} from "./BaseServer.js"; + +export class SetupServer extends BaseServer { + readonly gameFactory: () => GameServer + + constructor(deps: BaseServerDeps & { gameFactory: () => GameServer }) { + super(deps) + this.gameFactory = deps.gameFactory + } + + async _initInternal(): Promise { + this.slashcmd.registerCommand(new SetupCommand(this.slashcmd)) + this.slashcmd.on("componentInteraction", setupComponentInteractionHandler) + const gameHandler = new DiscordWebhookHandler({ + webhook: this.gameWebhook, + templateFilename: "setup/game.pug", + appId: this.appId, + secret: this.clientSecret, + destinationFunc: () => { + if (this.adminWebhook.isPresent) { + return "clear" + } else { + return "adminChannel" + } + }, + }) + const adminHandler = new DiscordWebhookHandler({ + webhook: this.adminWebhook, + templateFilename: "setup/admin.pug", + appId: this.appId, + secret: this.clientSecret, + destinationFunc: () => { + if (this.gameWebhook.isPresent) { + return "clear" + } else { + return "gameChannel" + } + }, + }) + this.server.get("/setup", async (req, res) => { + if (!this.gameWebhook.isPresent) { + res.redirect(`setup/gameChannel`) + } else if (!this.adminWebhook.isPresent) { + res.redirect(`setup/adminChannel`) + } else { + res.redirect(`setup/done`) + } + }) + this.server.get("/setup/start", async (req, res) => { + res.redirect(".") + }) + this.server.get("/game", async (req, res) => { + res.redirect("setup") + }) + this.server.get("/game/started", async (req, res) => { + res.redirect("../setup") + }) + this.server.get("/game/stop", async (req, res) => { + res.redirect("../setup") + }) + this.server.get("/game/start", async (req, res) => { + if (!checkAndClearXSRFCookie(req, res)) { + return renderError({ + baseUrl: getBaseUrl(req), + res, + code: 400, + error: "Token was incorrect or not set.", + context: "starting the game", + buttonText: "Return to Setup", + buttonUrl: "setup" + }) + } + if (!this.gameWebhook.isPresent || !this.adminWebhook.isPresent) { + return renderError({ + baseUrl: getBaseUrl(req), + res, + code: 409, + error: "You can't start the game while one of the channels is not set!", + context: "starting the game", + buttonText: "Finish Setup", + buttonUrl: "setup" + }) + } + res.code(200) + res.type("text/html") + res.send(pug.renderFile("static/pages/setup/start.pug", { + startedUrl: "game/start" + })) + setImmediate(async () => { + this.server.log.info("Shutting down the setup server and switching to the game server.") + try { + await this.server.close() + } catch (e) { + this.server.log.error(e, "Failed to shut down the setup server") + } + try { + await this.gameFactory().initialize() + } catch (e) { + this.server.log.error(e, "Failed to start up the game server") + } + this.server.log.info("Successfully switched from the setup server to the game server.") + }) + + }) + this.server.get("/setup/gameChannel", async (req, res) => { + return await gameHandler.handleRequest(req, res) + }) + this.server.get("/setup/adminChannel", async (req, res) => { + return await adminHandler.handleRequest(req, res) + }) + this.server.get("/setup/clear", async (req, res) => { + if (!checkAndClearXSRFCookie(req, res)) { + return renderError({ + baseUrl: getBaseUrl(req), + res, + code: 400, + error: "Token was incorrect or not set.", + context: "clearing the channel setup", + buttonText: "Return to Setup", + buttonUrl: "setup" + }) + } + try { + await Promise.all([this.gameWebhook, this.adminWebhook] + .filter((item) => item.isPresent) + .map((item) => item.clearHook())) + } catch (e) { + return renderError({ + baseUrl: getBaseUrl(req), + res, + code: 500, + error: e, + context: "clearing and deleting the webhooks", + buttonUrl: "setup/clear", + buttonText: "Try Again" + }) + } + res.redirect(".") + }) + this.server.get("/setup/done", async (req, res) => { + if (!this.gameWebhook.isPresent) { + res.redirect("gameChannel") + } else if (!this.adminWebhook.isPresent) { + res.redirect("adminChannel") + } else { + const token = generateXSRFCookie(res) + res.code(200) + res.type("text/html") + res.send(pug.renderFile("static/pages/setup/done.pug", { + baseUrl: getBaseUrl(req), + gameModeUrl: `game/start?token=${token}`, + clearUrl: `setup/clear?token=${token}`, + gameSetupUrl: `setup/gameChannel`, + adminSetupUrl: `setup/adminChannel`, + shutdownUrl: `shutdown?token=${token}` + })) + } + }) + this.server.get("/shutdown", async (req, res) => { + if (!checkAndClearXSRFCookie(req, res)) { + return renderError({ + baseUrl: getBaseUrl(req), + res, + code: 400, + error: "Token was incorrect or not set.", + context: "shutting the server down", + buttonText: "Return to Setup", + buttonUrl: "setup" + }) + } + res.code(200) + res.type("text/html") + res.send(pug.renderFile("static/pages/shutdown.pug")) + setImmediate(async () => { + this.server.log.info("Shutting down the setup server.") + try { + await this.server.close() + } catch (e) { + this.server.log.error(e, "Failed to shut down the setup server") + } + this.server.log.info("Shut down. Good night...") + }) + }) + } +} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 35e3145..5eb5cf4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,5 +1,11 @@ import dotenv from "dotenv"; +import {SetupServer} from "./SetupServer.js"; import {GameServer} from "./GameServer.js"; +import cryptoRandomString from "crypto-random-string"; +import {SavedWebhook} from "./SavedWebhook.js"; +import pino from "pino" + +const log = pino() async function main(): Promise { const {parsed, error} = dotenv.config() @@ -8,14 +14,44 @@ async function main(): Promise { } const clientSecret = parsed["DISCORD_CLIENT_SECRET"] const appId = parsed["DISCORD_APP_ID"] - const port = parseInt(parsed["HTTP_PORT"] ?? "5244") - const server = new GameServer({ - appId, port, secret: clientSecret - }) - return await server.initialize() + const botToken = parsed["DISCORD_BOT_TOKEN"] + const publicKey = parsed["DISCORD_PUBLIC_KEY"] + const listenPort = parseInt(parsed["HTTP_PORT"] ?? "5244") + const listenAddress = parsed["HTTP_ADDRESS"] ?? "127.0.0.1" + const cookieSecret = parsed["COOKIE_SECRET"] ?? cryptoRandomString({length: 32, type: "base64"}) + const gameWebhook = new SavedWebhook("game.json", {logger: log}) + const adminWebhook = new SavedWebhook("admin.json", {logger: log}) + const factory: { game: () => GameServer, setup: () => SetupServer } = { + game(): never { + throw Error("game factory not set up yet") + }, + setup(): never { + throw Error("setup factory not set up yet") + }, + } + const deps = { + appId, + listenAddress, + listenPort, + clientSecret, + cookieSecret, + gameFactory: () => factory.game(), + setupFactory: () => factory.setup(), + gameWebhook, + adminWebhook, + botToken, + publicKey, + } + factory.setup = () => new SetupServer(deps) + factory.game = () => new GameServer(deps) + await Promise.all([gameWebhook.load(), adminWebhook.load()]) + if (gameWebhook.isPresent && adminWebhook.isPresent) { + await factory.game().initialize() + } else { + await factory.setup().initialize() + } } main().catch((err) => { - console.log("Main crashed!") - console.log(err) + log.fatal(err, "Startup failed!") }) diff --git a/src/commands/SetupCommand.ts b/src/commands/SetupCommand.ts new file mode 100644 index 0000000..8703f0c --- /dev/null +++ b/src/commands/SetupCommand.ts @@ -0,0 +1,28 @@ +import {AutocompleteContext, CommandContext, ComponentContext, Message, SlashCommand, SlashCreator} from "slash-create"; + +export class SetupCommand extends SlashCommand { + constructor(creator: SlashCreator) { + super(creator, { + name: "setup-commands-disabled", + unknown: true + }); + } + + autocomplete(ctx: AutocompleteContext): Promise { + return ctx.sendResults([]) + } + + run(ctx: CommandContext): Promise { + return ctx.send({ + ephemeral: true, + content: "The server is currently in setup mode! You can't run commands right now..." + }) + } +} + +export function setupComponentInteractionHandler(ctx: ComponentContext): Promise { + return ctx.send({ + ephemeral: true, + content: "The server is currently in setup mode! You can't interact with its messages right now..." + }) +} \ No newline at end of file diff --git a/src/commands/game/PullCommand.ts b/src/commands/game/PullCommand.ts new file mode 100644 index 0000000..a1fda32 --- /dev/null +++ b/src/commands/game/PullCommand.ts @@ -0,0 +1,56 @@ +import {CommandContext, CommandOptionType, SlashCommand, SlashCreator} from "slash-create"; +import {SavedWebhook} from "../../SavedWebhook.js"; +import {Chance} from "chance"; + +const rand = Chance() + +export class PullCommand extends SlashCommand { + readonly gameWebhook: SavedWebhook + + constructor(creator: SlashCreator, gameWebhook: SavedWebhook) { + super(creator, { + name: "pull", + guildIDs: gameWebhook.state?.guild_id, + description: "Pulls one or more new heroines from the ether.", + options: [ + { + name: "count", + description: "The number of heroines to pull.", + required: false, + max_value: 10, + min_value: 1, + type: CommandOptionType.NUMBER, + } + ] + }); + this.gameWebhook = gameWebhook + } + + run(ctx: CommandContext): Promise { + if (ctx.guildID !== this.gameWebhook.state?.guild_id) { + return ctx.send({ + content: "Sorry, you can't do that in this guild.", + ephemeral: true, + }) + } + if (ctx.channelID !== this.gameWebhook.state?.channel_id) { + return ctx.send({ + content: `Sorry, you can't do that here. You have to do it in <#${this.gameWebhook.state?.channel_id}>.`, + ephemeral: true, + }) + } + const count: number = ctx.options.count ?? 1 + const results: string[] = [] + for (let x = 0; x < count; x += 1) { + results.push(rand.weighted(["**Nicole**: D tier Predator Podcaster", + "**Herja**: C tier Viking Warrior", + "**Sharla**: B tier Skark Girl", + "**Melpomene**: A tier Muse of Tragedy", + "**Lady Bootstrap**: S tier Time Traveler"], [20, 15, 10, 5, 1])) + } + return ctx.send({ + content: `_${ctx.user.mention}_, you pulled...\n \\* ${results.join("\n \\* ")}`, + ephemeral: false, + }) + } +} \ No newline at end of file diff --git a/static/pages/error.pug b/static/pages/error.pug deleted file mode 100644 index 8a95d15..0000000 --- a/static/pages/error.pug +++ /dev/null @@ -1,10 +0,0 @@ -extends ../template/template - -block title - | Setup Error -block content - | Something went wrong while #{context}. What did you do? - br - | The error was: #{error} -block link - a(href="setup") Return to Setup Page \ No newline at end of file diff --git a/static/pages/game/running.pug b/static/pages/game/running.pug new file mode 100644 index 0000000..a324be5 --- /dev/null +++ b/static/pages/game/running.pug @@ -0,0 +1,11 @@ +extends ../../template/template + +block title + | Game Running +block content + | The game is up and running. + br + | To make changes to its configuration setup, click Return to Setup. +block link + a(href=setupModeUrl, rel="nofollow") Return to Setup + a(href=shutdownUrl, rel="nofollow") Shut Down Server \ No newline at end of file diff --git a/static/pages/game/stop.pug b/static/pages/game/stop.pug new file mode 100644 index 0000000..5e477c2 --- /dev/null +++ b/static/pages/game/stop.pug @@ -0,0 +1,13 @@ +extends ../../template/template + +block title + | Game Stopping +block content + | You got it! The game is being stopped and returned to the setup phase now... + | If you aren't automatically redirected after a few seconds, click the button below. + script. + setTimeout(() => { + document.getElementById("stopButton").click() + }, 3000) +block link + a(href=stoppedUrl, id="stopButton", rel="nofollow") Verify Game Stopped \ No newline at end of file diff --git a/static/pages/setupAdmin.pug b/static/pages/setup/admin.pug similarity index 91% rename from static/pages/setupAdmin.pug rename to static/pages/setup/admin.pug index 716b06e..9bf0028 100644 --- a/static/pages/setupAdmin.pug +++ b/static/pages/setup/admin.pug @@ -1,4 +1,4 @@ -extends ../template/template +extends ../../template/template block title | Admin Setup diff --git a/static/pages/setup/done.pug b/static/pages/setup/done.pug new file mode 100644 index 0000000..8328fba --- /dev/null +++ b/static/pages/setup/done.pug @@ -0,0 +1,14 @@ +extends ../../template/template + +block title + | Setup Complete! +block content + | You're all done! Time to just play the game. + br + | If you'd like to reconfigure the game's channels, click one of the buttons below. +block link + a(href=gameModeUrl, rel="nofollow") Start the Game! + a(href=gameSetupUrl, rel="nofollow") Reconfigure Game Channel + a(href=adminSetupUrl, rel="nofollow") Reconfigure Admin Channel + a(href=clearUrl, rel="nofollow") Clear All Channels + a(href=shutdownUrl, rel="nofollow") Shut Down Server \ No newline at end of file diff --git a/static/pages/setup/error.pug b/static/pages/setup/error.pug new file mode 100644 index 0000000..50074b9 --- /dev/null +++ b/static/pages/setup/error.pug @@ -0,0 +1,13 @@ +extends ../../template/template + +block title + | Setup Error +block content + | Something went wrong while #{context}. What did you do? + br + if errorUrl + | The error was: #[a(href=errorUrl) #{error}] + else + | The error was: #{error} +block link + a(href=buttonUrl, rel="nofollow") #{buttonText} \ No newline at end of file diff --git a/static/pages/setupGame.pug b/static/pages/setup/game.pug similarity index 87% rename from static/pages/setupGame.pug rename to static/pages/setup/game.pug index e057c19..5769964 100644 --- a/static/pages/setupGame.pug +++ b/static/pages/setup/game.pug @@ -1,4 +1,4 @@ -extends ../template/template +extends ../../template/template block title | Game Setup @@ -15,4 +15,4 @@ block content br | Click the link to choose a game channel. block link - a(href=authUrl) Connect Game Channel \ No newline at end of file + a(href=authUrl, rel="nofollow") Connect Game Channel \ No newline at end of file diff --git a/static/pages/setup/start.pug b/static/pages/setup/start.pug new file mode 100644 index 0000000..dff3970 --- /dev/null +++ b/static/pages/setup/start.pug @@ -0,0 +1,13 @@ +extends ../../template/template + +block title + | Game Starting +block content + | You got it! The game is being started up now... + | If you aren't automatically redirected after a few seconds, click the button below. + script. + setTimeout(() => { + document.getElementById("startButton").click() + }, 3000) +block link + a(href=startedUrl, id="startButton", rel="nofollow") Verify Game Started \ No newline at end of file diff --git a/static/pages/setupDone.pug b/static/pages/setupDone.pug deleted file mode 100644 index 05339e1..0000000 --- a/static/pages/setupDone.pug +++ /dev/null @@ -1,12 +0,0 @@ -extends ../template/template - -block title - | Setup Complete! -block content - | You're all done! Time to just play the game. - br - | If you'd like to reconfigure the game's channels, click one of the buttons below. -block link - a(href=gameSetupUrl) Reconfigure Game Channel - a(href=adminSetupUrl) Reconfigure Admin Channel - a(href=clearUrl) Clear All Channels \ No newline at end of file diff --git a/static/pages/shutdown.pug b/static/pages/shutdown.pug new file mode 100644 index 0000000..c48c4ca --- /dev/null +++ b/static/pages/shutdown.pug @@ -0,0 +1,6 @@ +extends ../template/template + +block title + | Server Shutdown +block content + | You got it. The server has been shut down. Good night... \ No newline at end of file diff --git a/static/template/template.pug b/static/template/template.pug index b7046c5..1f09fda 100644 --- a/static/template/template.pug +++ b/static/template/template.pug @@ -4,13 +4,51 @@ html block title base(href=baseUrl) style. - body { display: flex; flex-flow: column; justify-content: center; align-items: center; min-height: 100vh; font-family: sans-serif; background-color: darkblue; color: aliceblue } - body * { max-width: 40em; } - p.content { text-align: center; } - p.link { display: flex; flex-flow: row; justify-content: center; align-items: center; } - p.link a { display: inline-block; border-radius: 5px; background-color: cadetblue; color: black; border: 2px solid darkslateblue; padding: 0.5em; box-shadow: 2px 4px 0 rgba(255,255,255,0.55); } - a:visited, a:active, a:link { color: inherit; text-decoration: none; } - p.link a:active { transform: translate(2px, 4px); box-shadow: 0 0 transparent; } + body { + display: flex; + flex-flow: column; + justify-content: center; + align-items: center; + min-height: 100vh; + font-family: sans-serif; + background-color: darkblue; + color: aliceblue + } + + body * { + max-width: 40em; + } + + p.content { + text-align: center; + } + + p.link { + display: flex; + flex-flow: row; + justify-content: center; + align-items: center; + } + + p.link a { + display: inline-block; + border-radius: 5px; + background-color: cadetblue; + color: black; + border: 2px solid darkslateblue; + padding: 0.5em; + box-shadow: 2px 4px 0 rgba(255, 255, 255, 0.55); + } + + p.link a:visited, p.link a:active, p.link a:link { + color: inherit; + text-decoration: none; + } + + p.link a:active { + transform: translate(2px, 4px); + box-shadow: 0 0 transparent; + } body h1 block title diff --git a/tsconfig.json b/tsconfig.json index f00bfbd..011d0f7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "ES2018", "lib": [ "dom", "dom.iterable",