diff --git a/.madgerc b/.madgerc new file mode 100644 index 0000000..4a7c725 --- /dev/null +++ b/.madgerc @@ -0,0 +1,7 @@ +{ + "detectiveOptions": { + "ts": { + "skipTypeImports": true + } + } +} \ No newline at end of file diff --git a/CONFIG.md b/CONFIG.md index 4af1da9..b2d351d 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -3,7 +3,7 @@ [<-- Click to return to README.md](./README.md) We recommend that you store the configuration file `config.json` in a safe place where Galvanic Corrosion can access it (the current directory).
-No other user on your server system should be able to access the file. +No other user on your host system should be able to access the configuration. ## Redis Redis is database software and must be installed for Galvanic Corrosion. @@ -19,26 +19,30 @@ If you are unsure of what this does, leave it unchanged. ## Network -Galvanic Corrosion listens on two ports: -* 13370/tcp(http) - for web endpoints -* 13371/tcp(http+ws) - for websockets +### Some issues may appear when connecting directly to a GC server's listening address. +Sockets behave erratically when connected directly to clients. This is a suspected issue with Deno websockets.
+For now, it is recommended that you use a middleman/proxy with your server. (see below) + +Galvanic Corrosion listens on two ports by default: +* 13370/tcp (http) +* 13371/tcp (http+ws) Currently, HTTPS and WSS are unsupported *directly* on GC. You can use a compatible reverse proxy solution to secure your server.
[Cloudflare Tunnels](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) (requires a domain with Cloudflare) are recommended. Port-forward or expose your server in some way. HTTPS is **strongly** recommended for your public address. -Once your server is reachable, the nameserver (and similar functions) need to know what the public "official" address of your server is.
-For example, your server listens on 10.0.0.6:13370(+13371) but is tunneled to my-gc-server(-socket).coolguy.xyz: +Once your server is reachable locally, the nameserver (and similar functions) need to know what the public "official" address of your server is.
+For example, your server listens on 10.0.0.6:13370 and 10.0.0.6:13371, but is tunneled to my-gc-server.coolguy.xyz and my-gc-server-socket.coolguy.xyz: - Set the "public host" for `web` and `socket` in `config.json` to the "official" address of your server * In the example, my-gc-server.coolguy.xyz and my-gc-server-socket.coolguy.xyz * This includes port numbers, but not the protocol - If your public address uses HTTPS (it should for proper authentication), enable `securepublichost` -You can test your configuration by navigating to `https://your-server.coolguy.xyz/ns`.
+You can test your configuration by navigating to `https://my-gc-server.coolguy.xyz/ns`. (use your server host)
Each field should contain your server's public address with an optional path at the end. -## Public Configuration +## Public This section contains basic information regarding your server. `serverName`: Somewhat invisible to players, but is an official label your server could appear as (to future server lists?) @@ -63,8 +67,12 @@ this can be anything *except* for "none" or 4, since there is only one server to `initialRoom`: On game startup, redirects the player to this room name instead of their DormRoom. Set to null if a "natural" startup is preferred.
This room must not be private and must be matchmakeable. +## General +`watchdogTimeout`: Terminate the server process after this number of milliseconds when SIGINT is emitted.
+This can help when your server does not shut down gracefully. + ## Logging -These three values expose booleans you can change to enable/disable logging various messages used for debugging or troubleshooting purposes. +These three booleans enable/disable logging various messages used for debugging or troubleshooting purposes. ## Discord Can be `null`. Currently unused. @@ -77,6 +85,8 @@ Parameters used by the server's authentication mechanisms. `secret`: Used to generate tokens. Should never be shared (the entire file) and can be a string of characters containing no words or patterns.
Use secure cryptography APIs in programming languages to generate random strings. +`console`: Key used to connect to the server console. Must be different than your `auth.secret`. + `timeout`: The maximum age for a token. `steamkey`: When not `null`, checks the Steam authentication ticket given by the client with the Steam User Auth API. Recommended for public servers. diff --git a/deno.json b/deno.json index 26b7433..221af73 100644 --- a/deno.json +++ b/deno.json @@ -7,17 +7,17 @@ "cross-compile": "deno run prebuild && deno run compile-win-a && deno run compile-linux-a && deno run postbuild", "dev": "deno run -A src/main.ts --dev", "prebuild": "deno run -A ./prebuild.ts", - "postbuild": "deno run -A ./postbuild.ts" + "postbuild": "deno run -A ./postbuild.ts", + "depcheck": "deno run -A npm:madge --circular --extensions ts ./src" }, "imports": { "@gz/jwt": "jsr:@gz/jwt@^0.1.0", - "@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.2.0", + "@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.3.0", "@types/cookie-parser": "npm:@types/cookie-parser@^1.4.8", "@types/express": "npm:@types/express@^5.0.0", "@types/multer": "npm:@types/multer@^1.4.12", "@types/validator": "npm:@types/validator@^13.12.2", "cookie-parser": "npm:cookie-parser@^1.4.7", - "discord.js": "npm:discord.js@^14.16.3", "express": "npm:express@^4.21.2", "ioredis": "npm:ioredis@^5.5.0", "multer": "npm:multer@^1.4.5-lts.2", @@ -31,5 +31,5 @@ "./src/types/http.ts" ] }, - "version": "0.1.0" + "version": "0.2.0" } diff --git a/deno.lock b/deno.lock index 256dcc4..02a979d 100644 --- a/deno.lock +++ b/deno.lock @@ -2,128 +2,107 @@ "version": "5", "specifiers": { "jsr:@gz/jwt@0.1": "0.1.0", - "jsr:@proxnet/undead-logging@^1.2.0": "1.2.0", - "jsr:@std/bytes@^1.0.2": "1.0.4", - "jsr:@std/crypto@^1.0.3": "1.0.3", - "jsr:@std/uuid@*": "1.0.4", - "npm:@imagemagick/magick-wasm@0.0.31": "0.0.31", - "npm:@types/cookie-parser@*": "1.4.8_@types+express@5.0.0", - "npm:@types/cookie-parser@^1.4.8": "1.4.8_@types+express@5.0.0", - "npm:@types/express@*": "5.0.0", - "npm:@types/express@5": "5.0.0", + "jsr:@proxnet/undead-logging@^1.3.0": "1.3.0", + "npm:@types/cookie-parser@^1.4.8": "1.4.8_@types+express@5.0.3", + "npm:@types/express@5": "5.0.3", "npm:@types/multer@^1.4.12": "1.4.12", - "npm:@types/node@*": "22.5.4", + "npm:@types/node@*": "22.15.15", "npm:@types/validator@^13.12.2": "13.12.2", - "npm:chalk@^5.3.0": "5.3.0", + "npm:chalk@^5.3.0": "5.4.1", "npm:cookie-parser@^1.4.7": "1.4.7", - "npm:discord.js@^14.16.3": "14.16.3", "npm:express@^4.21.2": "4.21.2", - "npm:ioredis@^5.5.0": "5.5.0", + "npm:ioredis@^5.5.0": "5.6.0", + "npm:madge@*": "8.0.0", "npm:multer@^1.4.5-lts.2": "1.4.5-lts.2", "npm:validator@^13.12.0": "13.12.0", - "npm:zod@^3.24.2": "3.24.2" + "npm:zod@^3.24.2": "3.25.8" }, "jsr": { "@gz/jwt@0.1.0": { "integrity": "32b0235cebcb85d363459b20ccaab0d8424fab89883c9f65caa1e2ad37e78e8f" }, - "@proxnet/undead-logging@1.2.0": { - "integrity": "59a4db428b5b848b7f51189b173b100ddabf7d86bb9de1a095e5d97b4a867e2c", + "@proxnet/undead-logging@1.3.0": { + "integrity": "313ac97bbbae9bab67d6220b18fd3cdb1e75bcdf47bf543c066c8b1a3375621a", "dependencies": [ "npm:chalk" ] - }, - "@std/bytes@1.0.4": { - "integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc" - }, - "@std/crypto@1.0.3": { - "integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f" - }, - "@std/uuid@1.0.4": { - "integrity": "f4233149cc8b4753cc3763fd83a7c4101699491f55c7be78dc7b30281946d7a0", - "dependencies": [ - "jsr:@std/bytes", - "jsr:@std/crypto" - ] } }, "npm": { - "@discordjs/builders@1.9.0": { - "integrity": "sha512-0zx8DePNVvQibh5ly5kCEei5wtPBIUbSoE9n+91Rlladz4tgtFbJ36PZMxxZrTEOQ7AHMZ/b0crT/0fCy6FTKg==", + "@babel/helper-string-parser@7.27.1": { + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" + }, + "@babel/helper-validator-identifier@7.27.1": { + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" + }, + "@babel/parser@7.27.2": { + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "dependencies": [ - "@discordjs/formatters", - "@discordjs/util", - "@sapphire/shapeshift", - "discord-api-types@0.37.97", - "fast-deep-equal", - "ts-mixer", - "tslib" + "@babel/types" + ], + "bin": true + }, + "@babel/types@7.27.1": { + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "dependencies": [ + "@babel/helper-string-parser", + "@babel/helper-validator-identifier" ] }, - "@discordjs/collection@1.5.3": { - "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==" - }, - "@discordjs/collection@2.1.1": { - "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==" - }, - "@discordjs/formatters@0.5.0": { - "integrity": "sha512-98b3i+Y19RFq1Xke4NkVY46x8KjJQjldHUuEbCqMvp1F5Iq9HgnGpu91jOi/Ufazhty32eRsKnnzS8n4c+L93g==", + "@dependents/detective-less@5.0.1": { + "integrity": "sha512-Y6+WUMsTFWE5jb20IFP4YGa5IrGY/+a/FbOSjDF/wz9gepU2hwCYSXRHP/vPwBvwcY3SVMASt4yXxbXNXigmZQ==", "dependencies": [ - "discord-api-types@0.37.97" + "gonzales-pe", + "node-source-walk" ] }, - "@discordjs/rest@2.4.0": { - "integrity": "sha512-Xb2irDqNcq+O8F0/k/NaDp7+t091p+acb51iA4bCKfIn+WFWd6HrNvcsSbMMxIR9NjcMZS6NReTKygqiQN+ntw==", - "dependencies": [ - "@discordjs/collection@2.1.1", - "@discordjs/util", - "@sapphire/async-queue", - "@sapphire/snowflake", - "@vladfrangu/async_event_emitter", - "discord-api-types@0.37.97", - "magic-bytes.js", - "tslib", - "undici" - ] - }, - "@discordjs/util@1.1.1": { - "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==" - }, - "@discordjs/ws@1.1.1": { - "integrity": "sha512-PZ+vLpxGCRtmr2RMkqh8Zp+BenUaJqlS6xhgWKEZcgC/vfHLEzpHtKkB0sl3nZWpwtcKk6YWy+pU3okL2I97FA==", - "dependencies": [ - "@discordjs/collection@2.1.1", - "@discordjs/rest", - "@discordjs/util", - "@sapphire/async-queue", - "@types/ws", - "@vladfrangu/async_event_emitter", - "discord-api-types@0.37.83", - "tslib", - "ws" - ] - }, - "@imagemagick/magick-wasm@0.0.31": { - "integrity": "sha512-QNivAUxSaItuiY8ziI/vRy6TtoecD7TOsD1LGZCG3wv8lfbdGbIj2QiJk0FlGkGwAVR966NlD3mkxPNvQrvq0w==" - }, "@ioredis/commands@1.2.0": { "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" }, - "@sapphire/async-queue@1.5.5": { - "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==" + "@jridgewell/sourcemap-codec@1.5.0": { + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, - "@sapphire/shapeshift@4.0.0": { - "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "@nodelib/fs.scandir@2.1.5": { + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dependencies": [ - "fast-deep-equal", - "lodash" + "@nodelib/fs.stat", + "run-parallel" ] }, - "@sapphire/snowflake@3.5.3": { - "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==" + "@nodelib/fs.stat@2.0.5": { + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==" }, - "@types/body-parser@1.19.5": { - "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "@nodelib/fs.walk@1.2.8": { + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": [ + "@nodelib/fs.scandir", + "fastq" + ] + }, + "@ts-graphviz/adapter@2.0.6": { + "integrity": "sha512-kJ10lIMSWMJkLkkCG5gt927SnGZcBuG0s0HHswGzcHTgvtUe7yk5/3zTEr0bafzsodsOq5Gi6FhQeV775nC35Q==", + "dependencies": [ + "@ts-graphviz/common" + ] + }, + "@ts-graphviz/ast@2.0.7": { + "integrity": "sha512-e6+2qtNV99UT6DJSoLbHfkzfyqY84aIuoV8Xlb9+hZAjgpum8iVHprGeAMQ4rF6sKUAxrmY8rfF/vgAwoPc3gw==", + "dependencies": [ + "@ts-graphviz/common" + ] + }, + "@ts-graphviz/common@2.1.5": { + "integrity": "sha512-S6/9+T6x8j6cr/gNhp+U2olwo1n0jKj/682QVqsh7yXWV6ednHYqxFw0ZsY3LyzT0N8jaZ6jQY9YD99le3cmvg==" + }, + "@ts-graphviz/core@2.0.7": { + "integrity": "sha512-w071DSzP94YfN6XiWhOxnLpYT3uqtxJBDYdh6Jdjzt+Ce6DNspJsPQgpC7rbts/B8tEkq0LHoYuIF/O5Jh5rPg==", + "dependencies": [ + "@ts-graphviz/ast", + "@ts-graphviz/common" + ] + }, + "@types/body-parser@1.19.6": { + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", "dependencies": [ "@types/connect", "@types/node" @@ -135,14 +114,14 @@ "@types/node" ] }, - "@types/cookie-parser@1.4.8_@types+express@5.0.0": { + "@types/cookie-parser@1.4.8_@types+express@5.0.3": { "integrity": "sha512-l37JqFrOJ9yQfRQkljb41l0xVphc7kg5JTjjr+pLRZ0IyZ49V4BQ8vbF4Ut2C2e+WH4al3xD3ZwYwIUfnbT4NQ==", "dependencies": [ "@types/express" ] }, - "@types/express-serve-static-core@5.0.1": { - "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "@types/express-serve-static-core@5.0.6": { + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", "dependencies": [ "@types/node", "@types/qs", @@ -150,17 +129,16 @@ "@types/send" ] }, - "@types/express@5.0.0": { - "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "@types/express@5.0.3": { + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dependencies": [ "@types/body-parser", "@types/express-serve-static-core", - "@types/qs", "@types/serve-static" ] }, - "@types/http-errors@2.0.4": { - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + "@types/http-errors@2.0.5": { + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==" }, "@types/mime@1.3.5": { "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" @@ -171,27 +149,27 @@ "@types/express" ] }, - "@types/node@22.5.4": { - "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", + "@types/node@22.15.15": { + "integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==", "dependencies": [ "undici-types" ] }, - "@types/qs@6.9.17": { - "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==" + "@types/qs@6.14.0": { + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==" }, "@types/range-parser@1.2.7": { "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, - "@types/send@0.17.4": { - "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "@types/send@0.17.5": { + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", "dependencies": [ "@types/mime", "@types/node" ] }, - "@types/serve-static@1.15.7": { - "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "@types/serve-static@1.15.8": { + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", "dependencies": [ "@types/http-errors", "@types/node", @@ -201,14 +179,70 @@ "@types/validator@13.12.2": { "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==" }, - "@types/ws@8.5.13": { - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "@typescript-eslint/types@8.32.1": { + "integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==" + }, + "@typescript-eslint/typescript-estree@8.32.1_typescript@5.8.3": { + "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", "dependencies": [ - "@types/node" + "@typescript-eslint/types", + "@typescript-eslint/visitor-keys", + "debug@4.4.1", + "fast-glob", + "is-glob", + "minimatch@9.0.5", + "semver", + "ts-api-utils", + "typescript" ] }, - "@vladfrangu/async_event_emitter@2.4.6": { - "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==" + "@typescript-eslint/visitor-keys@8.32.1": { + "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", + "dependencies": [ + "@typescript-eslint/types", + "eslint-visitor-keys" + ] + }, + "@vue/compiler-core@3.5.14": { + "integrity": "sha512-k7qMHMbKvoCXIxPhquKQVw3Twid3Kg4s7+oYURxLGRd56LiuHJVrvFKI4fm2AM3c8apqODPfVJGoh8nePbXMRA==", + "dependencies": [ + "@babel/parser", + "@vue/shared", + "entities", + "estree-walker", + "source-map-js" + ] + }, + "@vue/compiler-dom@3.5.14": { + "integrity": "sha512-1aOCSqxGOea5I80U2hQJvXYpPm/aXo95xL/m/mMhgyPUsKe9jhjwWpziNAw7tYRnbz1I61rd9Mld4W9KmmRoug==", + "dependencies": [ + "@vue/compiler-core", + "@vue/shared" + ] + }, + "@vue/compiler-sfc@3.5.14": { + "integrity": "sha512-9T6m/9mMr81Lj58JpzsiSIjBgv2LiVoWjIVa7kuXHICUi8LiDSIotMpPRXYJsXKqyARrzjT24NAwttrMnMaCXA==", + "dependencies": [ + "@babel/parser", + "@vue/compiler-core", + "@vue/compiler-dom", + "@vue/compiler-ssr", + "@vue/shared", + "estree-walker", + "magic-string", + "postcss", + "source-map-js" + ] + }, + "@vue/compiler-ssr@3.5.14": { + "integrity": "sha512-Y0G7PcBxr1yllnHuS/NxNCSPWnRGH4Ogrp0tsLA5QemDZuJLs99YjAKQ7KqkHE0vCg4QTKlQzXLKCMF7WPSl7Q==", + "dependencies": [ + "@vue/compiler-dom", + "@vue/shared" + ] + }, + "@vue/shared@3.5.14": { + "integrity": "sha512-oXTwNxVfc9EtP1zzXAlSlgARLXNC84frFYkS0HHz0h3E4WZSP9sywqjqzGCP9Y34M8ipNmd380pVgmMuwELDyQ==" }, "accepts@1.3.8": { "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", @@ -217,12 +251,44 @@ "negotiator" ] }, + "ansi-regex@5.0.1": { + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles@4.3.0": { + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": [ + "color-convert" + ] + }, + "any-promise@1.3.0": { + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "app-module-path@2.2.0": { + "integrity": "sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==" + }, "append-field@1.0.0": { "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" }, "array-flatten@1.1.1": { "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "ast-module-types@6.0.1": { + "integrity": "sha512-WHw67kLXYbZuHTmcdbIrVArCq5wxo6NEuj3hiYAWr8mwJeC+C2mMCIBIWCiDoCye/OF/xelc+teJ1ERoWmnEIA==" + }, + "balanced-match@1.0.2": { + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js@1.5.1": { + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bl@4.1.0": { + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": [ + "buffer", + "inherits", + "readable-stream@3.6.2" + ] + }, "body-parser@1.20.3": { "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": [ @@ -240,9 +306,35 @@ "unpipe" ] }, + "brace-expansion@1.1.11": { + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": [ + "balanced-match", + "concat-map" + ] + }, + "brace-expansion@2.0.1": { + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": [ + "balanced-match" + ] + }, + "braces@3.0.3": { + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": [ + "fill-range" + ] + }, "buffer-from@1.1.2": { "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "buffer@5.7.1": { + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dependencies": [ + "base64-js", + "ieee754" + ] + }, "busboy@1.6.0": { "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", "dependencies": [ @@ -252,32 +344,72 @@ "bytes@3.1.2": { "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, - "call-bind-apply-helpers@1.0.1": { - "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "call-bind-apply-helpers@1.0.2": { + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dependencies": [ "es-errors", "function-bind" ] }, - "call-bound@1.0.3": { - "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "call-bound@1.0.4": { + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dependencies": [ "call-bind-apply-helpers", "get-intrinsic" ] }, - "chalk@5.3.0": { - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" + "chalk@4.1.2": { + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": [ + "ansi-styles", + "supports-color" + ] + }, + "chalk@5.4.1": { + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==" + }, + "cli-cursor@3.1.0": { + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": [ + "restore-cursor" + ] + }, + "cli-spinners@2.9.2": { + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==" + }, + "clone@1.0.4": { + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==" }, "cluster-key-slot@1.1.2": { "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==" }, + "color-convert@2.0.1": { + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": [ + "color-name" + ] + }, + "color-name@1.1.4": { + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "commander@12.1.0": { + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==" + }, + "commander@7.2.0": { + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "commondir@1.0.1": { + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + }, + "concat-map@0.0.1": { + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, "concat-stream@1.6.2": { "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dependencies": [ "buffer-from", "inherits", - "readable-stream", + "readable-stream@2.3.8", "typedarray" ] }, @@ -315,45 +447,108 @@ "ms@2.0.0" ] }, - "debug@4.4.0": { - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "debug@4.4.1": { + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dependencies": [ "ms@2.1.3" ] }, + "deep-extend@0.6.0": { + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "defaults@1.0.4": { + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": [ + "clone" + ] + }, "denque@2.1.0": { "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" }, "depd@2.0.0": { "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, + "dependency-tree@11.1.1": { + "integrity": "sha512-pnkCd8VGOq70EVaEQxDC9mZCjCwYj4yG4j8h+PEJswuWp+rdE6p8zbtVvWk+yPwaVimOjlhNi782U9K5KOU9MQ==", + "dependencies": [ + "commander@12.1.0", + "filing-cabinet", + "precinct", + "typescript" + ], + "bin": true + }, "destroy@1.2.0": { "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, - "discord-api-types@0.37.100": { - "integrity": "sha512-a8zvUI0GYYwDtScfRd/TtaNBDTXwP5DiDVX7K5OmE+DRT57gBqKnwtOC5Ol8z0mRW8KQfETIgiB8U0YZ9NXiCA==" - }, - "discord-api-types@0.37.83": { - "integrity": "sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==" - }, - "discord-api-types@0.37.97": { - "integrity": "sha512-No1BXPcVkyVD4ZVmbNgDKaBoqgeQ+FJpzZ8wqHkfmBnTZig1FcH3iPPersiK1TUIAzgClh2IvOuVUYfcWLQAOA==" - }, - "discord.js@14.16.3": { - "integrity": "sha512-EPCWE9OkA9DnFFNrO7Kl1WHHDYFXu3CNVFJg63bfU7hVtjZGyhShwZtSBImINQRWxWP2tgo2XI+QhdXx28r0aA==", + "detective-amd@6.0.1": { + "integrity": "sha512-TtyZ3OhwUoEEIhTFoc1C9IyJIud3y+xYkSRjmvCt65+ycQuc3VcBrPRTMWoO/AnuCyOB8T5gky+xf7Igxtjd3g==", "dependencies": [ - "@discordjs/builders", - "@discordjs/collection@1.5.3", - "@discordjs/formatters", - "@discordjs/rest", - "@discordjs/util", - "@discordjs/ws", - "@sapphire/snowflake", - "discord-api-types@0.37.100", - "fast-deep-equal", - "lodash.snakecase", - "tslib", - "undici" + "ast-module-types", + "escodegen", + "get-amd-module-type", + "node-source-walk" + ], + "bin": true + }, + "detective-cjs@6.0.1": { + "integrity": "sha512-tLTQsWvd2WMcmn/60T2inEJNhJoi7a//PQ7DwRKEj1yEeiQs4mrONgsUtEJKnZmrGWBBmE0kJ1vqOG/NAxwaJw==", + "dependencies": [ + "ast-module-types", + "node-source-walk" + ] + }, + "detective-es6@5.0.1": { + "integrity": "sha512-XusTPuewnSUdoxRSx8OOI6xIA/uld/wMQwYsouvFN2LAg7HgP06NF1lHRV3x6BZxyL2Kkoih4ewcq8hcbGtwew==", + "dependencies": [ + "node-source-walk" + ] + }, + "detective-postcss@7.0.1_postcss@8.5.3": { + "integrity": "sha512-bEOVpHU9picRZux5XnwGsmCN4+8oZo7vSW0O0/Enq/TO5R2pIAP2279NsszpJR7ocnQt4WXU0+nnh/0JuK4KHQ==", + "dependencies": [ + "is-url", + "postcss", + "postcss-values-parser" + ] + }, + "detective-sass@6.0.1": { + "integrity": "sha512-jSGPO8QDy7K7pztUmGC6aiHkexBQT4GIH+mBAL9ZyBmnUIOFbkfZnO8wPRRJFP/QP83irObgsZHCoDHZ173tRw==", + "dependencies": [ + "gonzales-pe", + "node-source-walk" + ] + }, + "detective-scss@5.0.1": { + "integrity": "sha512-MAyPYRgS6DCiS6n6AoSBJXLGVOydsr9huwXORUlJ37K3YLyiN0vYHpzs3AdJOgHobBfispokoqrEon9rbmKacg==", + "dependencies": [ + "gonzales-pe", + "node-source-walk" + ] + }, + "detective-stylus@5.0.1": { + "integrity": "sha512-Dgn0bUqdGbE3oZJ+WCKf8Dmu7VWLcmRJGc6RCzBgG31DLIyai9WAoEhYRgIHpt/BCRMrnXLbGWGPQuBUrnF0TA==" + }, + "detective-typescript@14.0.0_typescript@5.8.3": { + "integrity": "sha512-pgN43/80MmWVSEi5LUuiVvO/0a9ss5V7fwVfrJ4QzAQRd3cwqU1SfWGXJFcNKUqoD5cS+uIovhw5t/0rSeC5Mw==", + "dependencies": [ + "@typescript-eslint/typescript-estree", + "ast-module-types", + "node-source-walk", + "typescript" + ] + }, + "detective-vue2@2.2.0_typescript@5.8.3": { + "integrity": "sha512-sVg/t6O2z1zna8a/UIV6xL5KUa2cMTQbdTIIvqNM0NIPswp52fe43Nwmbahzj3ww4D844u/vC2PYfiGLvD3zFA==", + "dependencies": [ + "@dependents/detective-less", + "@vue/compiler-sfc", + "detective-es6", + "detective-sass", + "detective-scss", + "detective-stylus", + "detective-typescript", + "typescript" ] }, "dunder-proto@1.0.1": { @@ -373,6 +568,16 @@ "encodeurl@2.0.0": { "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, + "enhanced-resolve@5.18.1": { + "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "dependencies": [ + "graceful-fs", + "tapable" + ] + }, + "entities@4.5.0": { + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, "es-define-property@1.0.1": { "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, @@ -388,6 +593,34 @@ "escape-html@1.0.3": { "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, + "escodegen@2.1.0": { + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dependencies": [ + "esprima", + "estraverse", + "esutils" + ], + "optionalDependencies": [ + "source-map" + ], + "bin": true + }, + "eslint-visitor-keys@4.2.0": { + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==" + }, + "esprima@4.0.1": { + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": true + }, + "estraverse@5.3.0": { + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + }, + "estree-walker@2.0.2": { + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "esutils@2.0.3": { + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, "etag@1.8.1": { "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, @@ -427,8 +660,44 @@ "vary" ] }, - "fast-deep-equal@3.1.3": { - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "fast-glob@3.3.3": { + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dependencies": [ + "@nodelib/fs.stat", + "@nodelib/fs.walk", + "glob-parent", + "merge2", + "micromatch" + ] + }, + "fastq@1.19.1": { + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dependencies": [ + "reusify" + ] + }, + "filing-cabinet@5.0.3": { + "integrity": "sha512-PlPcMwVWg60NQkhvfoxZs4wEHjhlOO/y7OAm4sKM60o1Z9nttRY4mcdQxp/iZ+kg/Vv6Hw1OAaTbYVM9DA9pYg==", + "dependencies": [ + "app-module-path", + "commander@12.1.0", + "enhanced-resolve", + "module-definition", + "module-lookup-amd", + "resolve", + "resolve-dependency-path", + "sass-lookup", + "stylus-lookup", + "tsconfig-paths", + "typescript" + ], + "bin": true + }, + "fill-range@7.1.1": { + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": [ + "to-regex-range" + ] }, "finalhandler@1.3.1": { "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", @@ -448,11 +717,21 @@ "fresh@0.5.2": { "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, + "fs.realpath@1.0.0": { + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, "function-bind@1.1.2": { "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, - "get-intrinsic@1.2.7": { - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "get-amd-module-type@6.0.1": { + "integrity": "sha512-MtjsmYiCXcYDDrGqtNbeIYdAl85n+5mSv2r3FbzER/YV3ZILw4HNNIw34HuV5pyl0jzs6GFYU1VHVEefhgcNHQ==", + "dependencies": [ + "ast-module-types", + "node-source-walk" + ] + }, + "get-intrinsic@1.3.0": { + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": [ "call-bind-apply-helpers", "es-define-property", @@ -466,6 +745,9 @@ "math-intrinsics" ] }, + "get-own-enumerable-property-symbols@3.0.2": { + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==" + }, "get-proto@1.0.1": { "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dependencies": [ @@ -473,9 +755,40 @@ "es-object-atoms" ] }, + "glob-parent@5.1.2": { + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": [ + "is-glob" + ] + }, + "glob@7.2.3": { + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": [ + "fs.realpath", + "inflight", + "inherits", + "minimatch@3.1.2", + "once", + "path-is-absolute" + ], + "deprecated": true + }, + "gonzales-pe@4.3.0": { + "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", + "dependencies": [ + "minimist" + ], + "bin": true + }, "gopd@1.2.0": { "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, + "graceful-fs@4.2.11": { + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "has-flag@4.0.0": { + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, "has-symbols@1.1.0": { "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, @@ -501,15 +814,29 @@ "safer-buffer" ] }, + "ieee754@1.2.1": { + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inflight@1.0.6": { + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": [ + "once", + "wrappy" + ], + "deprecated": true + }, "inherits@2.0.4": { "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "ioredis@5.5.0": { - "integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==", + "ini@1.3.8": { + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "ioredis@5.6.0": { + "integrity": "sha512-tBZlIIWbndeWBWCXWZiqtOF/yxf6yZX3tAlTJ7nfo5jhd6dctNxF7QnYlZLZ1a0o0pDoen7CgZqO+zjNaFbJAg==", "dependencies": [ "@ioredis/commands", "cluster-key-slot", - "debug@4.4.0", + "debug@4.4.1", "denque", "lodash.defaults", "lodash.isarguments", @@ -521,23 +848,85 @@ "ipaddr.js@1.9.1": { "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "is-core-module@2.16.1": { + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": [ + "hasown" + ] + }, + "is-extglob@2.1.1": { + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-glob@4.0.3": { + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": [ + "is-extglob" + ] + }, + "is-interactive@1.0.0": { + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==" + }, + "is-number@7.0.0": { + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-obj@1.0.1": { + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==" + }, + "is-regexp@1.0.0": { + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==" + }, + "is-unicode-supported@0.1.0": { + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" + }, + "is-url-superb@4.0.0": { + "integrity": "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==" + }, + "is-url@1.2.4": { + "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + }, "isarray@1.0.0": { "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, + "json5@2.2.3": { + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": true + }, "lodash.defaults@4.2.0": { "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, "lodash.isarguments@3.1.0": { "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, - "lodash.snakecase@4.1.1": { - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + "log-symbols@4.1.0": { + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": [ + "chalk@4.1.2", + "is-unicode-supported" + ] }, - "lodash@4.17.21": { - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "madge@8.0.0": { + "integrity": "sha512-9sSsi3TBPhmkTCIpVQF0SPiChj1L7Rq9kU2KDG1o6v2XH9cCw086MopjVCD+vuoL5v8S77DTbVopTO8OUiQpIw==", + "dependencies": [ + "chalk@4.1.2", + "commander@7.2.0", + "commondir", + "debug@4.4.1", + "dependency-tree", + "ora", + "pluralize", + "pretty-ms", + "rc", + "stream-to-array", + "ts-graphviz", + "walkdir" + ], + "bin": true }, - "magic-bytes.js@1.10.0": { - "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==" + "magic-string@0.30.17": { + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dependencies": [ + "@jridgewell/sourcemap-codec" + ] }, "math-intrinsics@1.1.0": { "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" @@ -548,9 +937,19 @@ "merge-descriptors@1.0.3": { "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, + "merge2@1.4.1": { + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + }, "methods@1.1.2": { "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, + "micromatch@4.0.8": { + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": [ + "braces", + "picomatch" + ] + }, "mime-db@1.52.0": { "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, @@ -564,6 +963,21 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "bin": true }, + "mimic-fn@2.1.0": { + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "minimatch@3.1.2": { + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": [ + "brace-expansion@1.1.11" + ] + }, + "minimatch@9.0.5": { + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dependencies": [ + "brace-expansion@2.0.1" + ] + }, "minimist@1.2.8": { "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" }, @@ -574,6 +988,24 @@ ], "bin": true }, + "module-definition@6.0.1": { + "integrity": "sha512-FeVc50FTfVVQnolk/WQT8MX+2WVcDnTGiq6Wo+/+lJ2ET1bRVi3HG3YlJUfqagNMc/kUlFSoR96AJkxGpKz13g==", + "dependencies": [ + "ast-module-types", + "node-source-walk" + ], + "bin": true + }, + "module-lookup-amd@9.0.4": { + "integrity": "sha512-DWJEuLVvjxh5b8wrvJC5wr2a7qo7pOWXIgdCBNazU416kcIyzO4drxvlqKhsHzYwxcC4cWuhoK+MiWCKCGnv7A==", + "dependencies": [ + "commander@12.1.0", + "glob", + "requirejs", + "requirejs-config-file" + ], + "bin": true + }, "ms@2.0.0": { "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, @@ -592,14 +1024,24 @@ "xtend" ] }, + "nanoid@3.3.11": { + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "bin": true + }, "negotiator@0.6.3": { "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, + "node-source-walk@7.0.1": { + "integrity": "sha512-3VW/8JpPqPvnJvseXowjZcirPisssnBuDikk6JIZ8jQzF7KJQX52iPFX4RYYxLycYH7IbMRSPUOga/esVjy5Yg==", + "dependencies": [ + "@babel/parser" + ] + }, "object-assign@4.1.1": { "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, - "object-inspect@1.13.3": { - "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==" + "object-inspect@1.13.4": { + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" }, "on-finished@2.4.1": { "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", @@ -607,12 +1049,100 @@ "ee-first" ] }, + "once@1.4.0": { + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": [ + "wrappy" + ] + }, + "onetime@5.1.2": { + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": [ + "mimic-fn" + ] + }, + "ora@5.4.1": { + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dependencies": [ + "bl", + "chalk@4.1.2", + "cli-cursor", + "cli-spinners", + "is-interactive", + "is-unicode-supported", + "log-symbols", + "strip-ansi", + "wcwidth" + ] + }, + "parse-ms@2.1.0": { + "integrity": "sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==" + }, "parseurl@1.3.3": { "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "path-is-absolute@1.0.1": { + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "path-parse@1.0.7": { + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, "path-to-regexp@0.1.12": { "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, + "picocolors@1.1.1": { + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch@2.3.1": { + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "pluralize@8.0.0": { + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==" + }, + "postcss-values-parser@6.0.2_postcss@8.5.3": { + "integrity": "sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==", + "dependencies": [ + "color-name", + "is-url-superb", + "postcss", + "quote-unquote" + ] + }, + "postcss@8.5.3": { + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dependencies": [ + "nanoid", + "picocolors", + "source-map-js" + ] + }, + "precinct@12.2.0_postcss@8.5.3_typescript@5.8.3": { + "integrity": "sha512-NFBMuwIfaJ4SocE9YXPU/n4AcNSoFMVFjP72nvl3cx69j/ke61/hPOWFREVxLkFhhEGnA8ZuVfTqJBa+PK3b5w==", + "dependencies": [ + "@dependents/detective-less", + "commander@12.1.0", + "detective-amd", + "detective-cjs", + "detective-es6", + "detective-postcss", + "detective-sass", + "detective-scss", + "detective-stylus", + "detective-typescript", + "detective-vue2", + "module-definition", + "node-source-walk", + "postcss", + "typescript" + ], + "bin": true + }, + "pretty-ms@7.0.1": { + "integrity": "sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==", + "dependencies": [ + "parse-ms" + ] + }, "process-nextick-args@2.0.1": { "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, @@ -629,6 +1159,12 @@ "side-channel" ] }, + "queue-microtask@1.2.3": { + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" + }, + "quote-unquote@1.0.0": { + "integrity": "sha512-twwRO/ilhlG/FIgYeKGFqyHhoEhqgnKVkcmqMKi2r524gz3ZbDTcyFt38E9xjJI2vT+KbRNHVbnJ/e0I25Azwg==" + }, "range-parser@1.2.1": { "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, @@ -641,6 +1177,16 @@ "unpipe" ] }, + "rc@1.2.8": { + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": [ + "deep-extend", + "ini", + "minimist", + "strip-json-comments" + ], + "bin": true + }, "readable-stream@2.3.8": { "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dependencies": [ @@ -653,6 +1199,14 @@ "util-deprecate" ] }, + "readable-stream@3.6.2": { + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": [ + "inherits", + "string_decoder", + "util-deprecate" + ] + }, "redis-errors@1.2.0": { "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==" }, @@ -662,6 +1216,45 @@ "redis-errors" ] }, + "requirejs-config-file@4.0.0": { + "integrity": "sha512-jnIre8cbWOyvr8a5F2KuqBnY+SDA4NXr/hzEZJG79Mxm2WiFQz2dzhC8ibtPJS7zkmBEl1mxSwp5HhC1W4qpxw==", + "dependencies": [ + "esprima", + "stringify-object" + ] + }, + "requirejs@2.3.7": { + "integrity": "sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw==", + "bin": true + }, + "resolve-dependency-path@4.0.1": { + "integrity": "sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ==" + }, + "resolve@1.22.10": { + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dependencies": [ + "is-core-module", + "path-parse", + "supports-preserve-symlinks-flag" + ], + "bin": true + }, + "restore-cursor@3.1.0": { + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": [ + "onetime", + "signal-exit" + ] + }, + "reusify@1.1.0": { + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==" + }, + "run-parallel@1.2.0": { + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dependencies": [ + "queue-microtask" + ] + }, "safe-buffer@5.1.2": { "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, @@ -671,6 +1264,18 @@ "safer-buffer@2.1.2": { "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sass-lookup@6.1.0": { + "integrity": "sha512-Zx+lVyoWqXZxHuYWlTA17Z5sczJ6braNT2C7rmClw+c4E7r/n911Zwss3h1uHI9reR5AgHZyNHF7c2+VIp5AUA==", + "dependencies": [ + "commander@12.1.0", + "enhanced-resolve" + ], + "bin": true + }, + "semver@7.7.1": { + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "bin": true + }, "send@0.19.0": { "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": [ @@ -737,12 +1342,27 @@ "side-channel-weakmap" ] }, + "signal-exit@3.0.7": { + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "source-map-js@1.2.1": { + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "source-map@0.6.1": { + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, "standard-as-callback@2.1.0": { "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" }, "statuses@2.0.1": { "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "stream-to-array@2.3.0": { + "integrity": "sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==", + "dependencies": [ + "any-promise" + ] + }, "streamsearch@1.1.0": { "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" }, @@ -752,14 +1372,76 @@ "safe-buffer@5.1.2" ] }, + "stringify-object@3.3.0": { + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dependencies": [ + "get-own-enumerable-property-symbols", + "is-obj", + "is-regexp" + ] + }, + "strip-ansi@6.0.1": { + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": [ + "ansi-regex" + ] + }, + "strip-bom@3.0.0": { + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==" + }, + "strip-json-comments@2.0.1": { + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" + }, + "stylus-lookup@6.1.0": { + "integrity": "sha512-5QSwgxAzXPMN+yugy61C60PhoANdItfdjSEZR8siFwz7yL9jTmV0UBKDCfn3K8GkGB4g0Y9py7vTCX8rFu4/pQ==", + "dependencies": [ + "commander@12.1.0" + ], + "bin": true + }, + "supports-color@7.2.0": { + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": [ + "has-flag" + ] + }, + "supports-preserve-symlinks-flag@1.0.0": { + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" + }, + "tapable@2.2.2": { + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==" + }, + "to-regex-range@5.0.1": { + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": [ + "is-number" + ] + }, "toidentifier@1.0.1": { "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, - "ts-mixer@6.0.4": { - "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==" + "ts-api-utils@2.1.0_typescript@5.8.3": { + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dependencies": [ + "typescript" + ] }, - "tslib@2.8.1": { - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + "ts-graphviz@2.1.6": { + "integrity": "sha512-XyLVuhBVvdJTJr2FJJV2L1pc4MwSjMhcunRVgDE9k4wbb2ee7ORYnPewxMWUav12vxyfUM686MSGsqnVRIInuw==", + "dependencies": [ + "@ts-graphviz/adapter", + "@ts-graphviz/ast", + "@ts-graphviz/common", + "@ts-graphviz/core" + ] + }, + "tsconfig-paths@4.2.0": { + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dependencies": [ + "json5", + "minimist", + "strip-bom" + ] }, "type-is@1.6.18": { "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", @@ -771,11 +1453,12 @@ "typedarray@0.0.6": { "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, - "undici-types@6.19.8": { - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + "typescript@5.8.3": { + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "bin": true }, - "undici@6.19.8": { - "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==" + "undici-types@6.21.0": { + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, "unpipe@1.0.0": { "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" @@ -792,120 +1475,26 @@ "vary@1.1.2": { "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, - "ws@8.18.0": { - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==" + "walkdir@0.4.1": { + "integrity": "sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==" + }, + "wcwidth@1.0.1": { + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": [ + "defaults" + ] + }, + "wrappy@1.0.2": { + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "xtend@4.0.2": { "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, - "zod@3.24.2": { - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==" + "zod@3.25.8": { + "integrity": "sha512-iJPWX8HoZ2VE21VrhHGU9jVo/kVDUQyqM9vF0MxDhW/fp2sAl1eVwGJgiYZdHGiMwQJImXIW80lKk0MnfDxqiQ==" } }, - "redirects": { - "https://deno.land/x/imagemagick_deno/mod.ts": "https://deno.land/x/imagemagick_deno@0.0.31/mod.ts" - }, "remote": { - "https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74", - "https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49", - "https://deno.land/std@0.140.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d", - "https://deno.land/std@0.140.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9", - "https://deno.land/std@0.140.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf", - "https://deno.land/std@0.140.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37", - "https://deno.land/std@0.140.0/fs/_util.ts": "0fb24eb4bfebc2c194fb1afdb42b9c3dda12e368f43e8f2321f84fc77d42cb0f", - "https://deno.land/std@0.140.0/fs/ensure_dir.ts": "9dc109c27df4098b9fc12d949612ae5c9c7169507660dcf9ad90631833209d9d", - "https://deno.land/std@0.140.0/hash/sha256.ts": "803846c7a5a8a5a97f31defeb37d72f519086c880837129934f5d6f72102a8e8", - "https://deno.land/std@0.140.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b", - "https://deno.land/std@0.140.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3", - "https://deno.land/std@0.140.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09", - "https://deno.land/std@0.140.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b", - "https://deno.land/std@0.140.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633", - "https://deno.land/std@0.140.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee", - "https://deno.land/std@0.140.0/path/mod.ts": "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d", - "https://deno.land/std@0.140.0/path/posix.ts": "293cdaec3ecccec0a9cc2b534302dfe308adb6f10861fa183275d6695faace44", - "https://deno.land/std@0.140.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9", - "https://deno.land/std@0.140.0/path/win32.ts": "31811536855e19ba37a999cd8d1b62078235548d67902ece4aa6b814596dd757", - "https://deno.land/std@0.140.0/streams/conversion.ts": "712585bfa0172a97fb68dd46e784ae8ad59d11b88079d6a4ab098ff42e697d21", - "https://deno.land/std@0.186.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462", - "https://deno.land/std@0.186.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", - "https://deno.land/std@0.186.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", - "https://deno.land/std@0.186.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", - "https://deno.land/std@0.186.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", - "https://deno.land/std@0.186.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", - "https://deno.land/std@0.186.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", - "https://deno.land/std@0.186.0/path/mod.ts": "ee161baec5ded6510ee1d1fb6a75a0f5e4b41f3f3301c92c716ecbdf7dae910d", - "https://deno.land/std@0.186.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", - "https://deno.land/std@0.186.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", - "https://deno.land/std@0.186.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba", - "https://deno.land/std@0.197.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", - "https://deno.land/std@0.197.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", - "https://deno.land/std@0.197.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", - "https://deno.land/std@0.197.0/fs/_util.ts": "fbf57dcdc9f7bc8128d60301eece608246971a7836a3bb1e78da75314f08b978", - "https://deno.land/std@0.197.0/fs/copy.ts": "b4f7fe87190d7b310c88a2d9ff845210c0a2b7b0a094ec509747359023beb7d6", - "https://deno.land/std@0.197.0/fs/empty_dir.ts": "c3d2da4c7352fab1cf144a1ecfef58090769e8af633678e0f3fabaef98594688", - "https://deno.land/std@0.197.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40", - "https://deno.land/std@0.197.0/fs/ensure_file.ts": "c38602670bfaf259d86ca824a94e6cb9e5eb73757fefa4ebf43a90dd017d53d9", - "https://deno.land/std@0.197.0/fs/ensure_link.ts": "c0f5b2f0ec094ed52b9128eccb1ee23362a617457aa0f699b145d4883f5b2fb4", - "https://deno.land/std@0.197.0/fs/ensure_symlink.ts": "5006ab2f458159c56d689b53b1e48d57e05eeb1eaf64e677f7f76a30bc4fdba1", - "https://deno.land/std@0.197.0/fs/eol.ts": "f1f2eb348a750c34500741987b21d65607f352cf7205f48f4319d417fff42842", - "https://deno.land/std@0.197.0/fs/exists.ts": "29c26bca8584a22876be7cb8844f1b6c8fc35e9af514576b78f5c6884d7ed02d", - "https://deno.land/std@0.197.0/fs/expand_glob.ts": "3e427436f4b3768727bd7de84169f10db75fe50b32e6dde567b8ae558a8d857a", - "https://deno.land/std@0.197.0/fs/mod.ts": "bc3d0acd488cc7b42627044caf47d72019846d459279544e1934418955ba4898", - "https://deno.land/std@0.197.0/fs/move.ts": "b4f8f46730b40c32ea3c0bc8eb0fd0e8139249a698883c7b3756424cf19785c9", - "https://deno.land/std@0.197.0/fs/walk.ts": "21a3cc5ff39c38acc93575213f54d5f1d44c5c6614ed97603d171eb0bf56a565", - "https://deno.land/std@0.197.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", - "https://deno.land/std@0.197.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", - "https://deno.land/std@0.197.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0", - "https://deno.land/std@0.197.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", - "https://deno.land/std@0.197.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1", - "https://deno.land/std@0.197.0/path/mod.ts": "f065032a7189404fdac3ad1a1551a9ac84751d2f25c431e101787846c86c79ef", - "https://deno.land/std@0.197.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d", - "https://deno.land/std@0.197.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1", - "https://deno.land/std@0.197.0/path/win32.ts": "4fca292f8d116fd6d62f243b8a61bd3d6835a9f0ede762ba5c01afe7c3c0aa12", - "https://deno.land/std@0.85.0/_util/assert.ts": "2f868145a042a11d5ad0a3c748dcf580add8a0dbc0e876eaa0026303a5488f58", - "https://deno.land/std@0.85.0/_util/os.ts": "e282950a0eaa96760c0cf11e7463e66babd15ec9157d4c9ed49cc0925686f6a7", - "https://deno.land/std@0.85.0/fs/_util.ts": "f2ce811350236ea8c28450ed822a5f42a0892316515b1cd61321dec13569c56b", - "https://deno.land/std@0.85.0/fs/copy.ts": "acc21e2569c92e715be48f40665a299cb995a4dce04145c3dd624791b885114c", - "https://deno.land/std@0.85.0/fs/empty_dir.ts": "2edd70ff6405e1893e781a82aec8c574dfc748a7bb9d9ce8f0abdf002cdbba3f", - "https://deno.land/std@0.85.0/fs/ensure_dir.ts": "f21262e788a707aaa2dd22064da7cd40e3b2f0f067e9b2aed1b288091170cc05", - "https://deno.land/std@0.85.0/fs/ensure_file.ts": "84c7cff81ecedef3969e3fcd2d0c2aecd9bafea246cd18847deba7a54126134f", - "https://deno.land/std@0.85.0/fs/ensure_link.ts": "e48abe5bf639389ee6f42bb8bdd8b7b2a4c93701cd618b12cdcad83ccea44f2e", - "https://deno.land/std@0.85.0/fs/ensure_symlink.ts": "cbb2c908135808c0545c6304046b6ab5c024b0bb1832e69c819b58d9feee66ef", - "https://deno.land/std@0.85.0/fs/eol.ts": "afaebaaac36f48c423b920c836551997715672b80a0fee9aa7667c181a94f2df", - "https://deno.land/std@0.85.0/fs/exists.ts": "b0d2e31654819cc2a8d37df45d6b14686c0cc1d802e9ff09e902a63e98b85a00", - "https://deno.land/std@0.85.0/fs/expand_glob.ts": "b5a8fcadf40eb7b034a1f807349cbace0ddb28c4e5a6b6aaf2d8ca925ba02f9f", - "https://deno.land/std@0.85.0/fs/mod.ts": "26eee4b52a8c516e37d464094b080ff6822883e7f01ff0ba0a72b8dcd54b9927", - "https://deno.land/std@0.85.0/fs/move.ts": "36697916a5cf2ebc7d298089a9a3ccc6b3af1eaecc173e57a9f5eb10f1f04221", - "https://deno.land/std@0.85.0/fs/walk.ts": "8d37f2164a7397668842a7cb5d53b9e7bcd216462623b1b96abe519f76d7f8b9", - "https://deno.land/std@0.85.0/path/_constants.ts": "1247fee4a79b70c89f23499691ef169b41b6ccf01887a0abd131009c5581b853", - "https://deno.land/std@0.85.0/path/_interface.ts": "1fa73b02aaa24867e481a48492b44f2598cd9dfa513c7b34001437007d3642e4", - "https://deno.land/std@0.85.0/path/_util.ts": "f4fa69aa3cbbd8568763bfc43c7236875015ba343602d8bafd332b4b4243681b", - "https://deno.land/std@0.85.0/path/common.ts": "eaf03d08b569e8a87e674e4e265e099f237472b6fd135b3cbeae5827035ea14a", - "https://deno.land/std@0.85.0/path/glob.ts": "4a524c1c9da3e79a9fdabdc6e850cd9e41bdf31e442856ffa19c5b123268ca95", - "https://deno.land/std@0.85.0/path/mod.ts": "4465dc494f271b02569edbb4a18d727063b5dbd6ed84283ff906260970a15d12", - "https://deno.land/std@0.85.0/path/posix.ts": "1408f8ba482a4dc5fc0a7cd7be28bbbff9608d2b3b5ffdcf288ae1228d959add", - "https://deno.land/std@0.85.0/path/separator.ts": "8fdcf289b1b76fd726a508f57d3370ca029ae6976fcde5044007f062e643ff1c", - "https://deno.land/std@0.85.0/path/win32.ts": "6ca052f54500f00cd7a5172fde62900626ab620dcd5bdcf4e6f5695d001ddef6", - "https://deno.land/x/bcrypt@v0.3.0/mod.ts": "ff09bdae282583cf5f7d87efe37ddcecef7f14f6d12e8b8066a3058db8c6c2f7", - "https://deno.land/x/bcrypt@v0.3.0/src/bcrypt/base64.ts": "b8266450a4f1eb6960f60f2f7986afc4dde6b45bd2d7ee7ba10789e67e17b9f7", - "https://deno.land/x/bcrypt@v0.3.0/src/bcrypt/bcrypt.ts": "65819ce8e32d6e6a68f8753931237c58baa39b2573c1d7fac42f03d51499f242", - "https://deno.land/x/bcrypt@v0.3.0/src/main.ts": "08d201b289c8d9c46f8839c69cd6625b213863db29775c7a200afc3b540e64f8", - "https://deno.land/x/bcrypt@v0.3.0/src/worker.ts": "5a73bdfee9c9e622f47c9733d374b627dce52fb3ec1e74c8226698b3fc57ffac", - "https://deno.land/x/deno_cache@0.4.1/auth_tokens.ts": "5fee7e9155e78cedf3f6ff3efacffdb76ac1a76c86978658d9066d4fb0f7326e", - "https://deno.land/x/deno_cache@0.4.1/cache.ts": "51f72f4299411193d780faac8c09d4e8cbee951f541121ef75fcc0e94e64c195", - "https://deno.land/x/deno_cache@0.4.1/deno_dir.ts": "f2a9044ce8c7fe1109004cda6be96bf98b08f478ce77e7a07f866eff1bdd933f", - "https://deno.land/x/deno_cache@0.4.1/deps.ts": "8974097d6c17e65d9a82d39377ae8af7d94d74c25c0cbb5855d2920e063f2343", - "https://deno.land/x/deno_cache@0.4.1/dirs.ts": "d2fa473ef490a74f2dcb5abb4b9ab92a48d2b5b6320875df2dee64851fa64aa9", - "https://deno.land/x/deno_cache@0.4.1/disk_cache.ts": "1f3f5232cba4c56412d93bdb324c624e95d5dd179d0578d2121e3ccdf55539f9", - "https://deno.land/x/deno_cache@0.4.1/file_fetcher.ts": "07a6c5f8fd94bf50a116278cc6012b4921c70d2251d98ce1c9f3c352135c39f7", - "https://deno.land/x/deno_cache@0.4.1/http_cache.ts": "f632e0d6ec4a5d61ae3987737a72caf5fcdb93670d21032ddb78df41131360cd", - "https://deno.land/x/deno_cache@0.4.1/mod.ts": "ef1cda9235a93b89cb175fe648372fc0f785add2a43aa29126567a05e3e36195", - "https://deno.land/x/deno_cache@0.4.1/util.ts": "8cb686526f4be5205b92c819ca2ce82220aa0a8dd3613ef0913f6dc269dbbcfe", - "https://deno.land/x/dir@1.5.1/data_local_dir/mod.ts": "91eb1c4bfadfbeda30171007bac6d85aadacd43224a5ed721bbe56bc64e9eb66", - "https://deno.land/x/emit@0.25.0/_utils.ts": "98412edc7aa29e77d592b54fbad00bdec1b05d0c25eb772a5f8edc9813e08d88", - "https://deno.land/x/emit@0.25.0/emit.generated.js": "0728e0cd293b930db2532f8cb5087fdb77aee1f30a059207533780f40250fd6a", - "https://deno.land/x/emit@0.25.0/mod.ts": "66ef8ddaedcfca033eeee851379af59ed3f0e0aa6e025e7cdd24e4e158d874f3", - "https://deno.land/x/imagemagick_deno@0.0.31/mod.ts": "124d7f045429f6e6c486b86e72d025410d09576bc0d8075e69f97118a1a33413", "https://deno.land/x/imagescript@1.3.0/ImageScript.js": "cf90773c966031edd781ed176c598f7ed495e7694cd9b86c986d2d97f783cca0", "https://deno.land/x/imagescript@1.3.0/mod.ts": "18a6cb83c55e690c873505f6fe867364c678afb64934fe7aef593a6b92f79995", "https://deno.land/x/imagescript@1.3.0/png/src/crc.mjs": "5cf50de181d61dd00e66a240d811018ba5070afa8bba302f393604404604de84", @@ -933,26 +1522,17 @@ "https://deno.land/x/imagescript@1.3.0/v2/ops/overlay.mjs": "7e6e2c2ffd25006d52597ab8babc5f8f503d388a3fdf2fbc0eaea02799a020c9", "https://deno.land/x/imagescript@1.3.0/v2/ops/resize.mjs": "814e78ebce8eaf8f1f918688db7b52a141405e06a36ed4b25d04413d69e7d17b", "https://deno.land/x/imagescript@1.3.0/v2/ops/rotate.mjs": "a1b65616717bd2eed8db406affea3263b4674dada46b56441ef38167a187455d", - "https://deno.land/x/imagescript@1.3.0/v2/util/mem.mjs": "4968d400dae069b4bf0ef4767c1802fd2cc7d15d90eda4cfadf5b4cd19b96c6d", - "https://deno.land/x/leaf@v1.0.4/constants.ts": "2b18c5be5a57cea4d3d6298d7c4c636e5db821c580c3197f9c9bcab65f8c3bf0", - "https://deno.land/x/leaf@v1.0.4/functions/getFileInMem.ts": "cec6c3c6add22c0c3316d8301994ab583feac5c3052df3072ad12976ea2aeec4", - "https://deno.land/x/leaf@v1.0.4/functions/getFilePath.ts": "80ce141c1bd9735d3b7961b6ec8736070475c296c342be6bb4e189483f020801", - "https://deno.land/x/leaf@v1.0.4/functions/methods.ts": "b8beebdcc1c0fbae00cc61dc3fdca7209a5b5e08c1b955ccf0e6b04c85c6ee46", - "https://deno.land/x/leaf@v1.0.4/leafCompiler.ts": "155ac29c04fe3a0f4d336a95058eebb1a14e291c31862356d11cd280e67563ce", - "https://deno.land/x/leaf@v1.0.4/mod.ts": "2f7a4d2c804978c342a2092ff1d2ec05ffb0e52dfc57bdeac939d56df8254983", - "https://deno.land/x/wasmbuild@0.14.1/cache.ts": "89eea5f3ce6035a1164b3e655c95f21300498920575ade23161421f5b01967f4", - "https://deno.land/x/wasmbuild@0.14.1/loader.ts": "d98d195a715f823151cbc8baa3f32127337628379a02d9eb2a3c5902dbccfc02" + "https://deno.land/x/imagescript@1.3.0/v2/util/mem.mjs": "4968d400dae069b4bf0ef4767c1802fd2cc7d15d90eda4cfadf5b4cd19b96c6d" }, "workspace": { "dependencies": [ "jsr:@gz/jwt@0.1", - "jsr:@proxnet/undead-logging@^1.2.0", + "jsr:@proxnet/undead-logging@^1.3.0", "npm:@types/cookie-parser@^1.4.8", "npm:@types/express@5", "npm:@types/multer@^1.4.12", "npm:@types/validator@^13.12.2", "npm:cookie-parser@^1.4.7", - "npm:discord.js@^14.16.3", "npm:express@^4.21.2", "npm:ioredis@^5.5.0", "npm:multer@^1.4.5-lts.2", diff --git a/postbuild.ts b/postbuild.ts index 1851e03..2ae9ed3 100644 --- a/postbuild.ts +++ b/postbuild.ts @@ -16,8 +16,8 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ try { - Deno.removeSync('./ver.ts'); - Deno.renameSync('./ver.ts.bak', 'ver.ts'); + await Deno.remove('./ver.ts'); + await Deno.rename('./ver.ts.bak', 'ver.ts'); } catch (err) { console.error(`Cannot post-build version information: ${err}`); Deno.exit(1); diff --git a/prebuild.ts b/prebuild.ts index 47b8dfb..f7c60d8 100644 --- a/prebuild.ts +++ b/prebuild.ts @@ -28,9 +28,9 @@ try { const newVerString = `${file.version}-${new TextDecoder().decode(commitHash.stdout).trim()}`; if (file.version) { - Deno.writeTextFileSync('./ver.ts.bak', devVer); - Deno.writeTextFileSync('./ver.ts', devVer.replace('development', newVerString)); - console.info('Built version information'); + await Deno.writeTextFile('./ver.ts.bak', devVer); + await Deno.writeTextFile('./ver.ts', devVer.replace('development', newVerString)); + console.info(`Built version information: Commit ${newVerString}`); } } catch (err) { console.error(`Cannot build version information: ${err}`); diff --git a/src/apiutils.ts b/src/apiutils.ts index 52ad9d7..f108a1e 100644 --- a/src/apiutils.ts +++ b/src/apiutils.ts @@ -19,12 +19,14 @@ along with this program. If not, see . */ import express from "express"; import Logging from "@proxnet/undead-logging"; import { decode } from "@gz/jwt"; -import { Config } from "./config.ts"; -import { AuthType, User, UserTokenFormat } from "./data/users.ts"; -import { ProfileTokenFormat } from "./data/profiles.ts"; +import { Config } from "./config/config.ts"; +import { User } from "./data/users.ts"; +import { AuthType } from "./data/UserTypes.ts"; import z from "zod"; import Matchmaking from "./data/live/base.ts"; -import Server from "./data/server.ts"; +import Server from "./data/server/server.ts"; +import { TokenSchema } from "./data/auth/TokenSchema.ts"; +import type { TokenFormat } from "./data/auth/TokenBaseFormat.ts"; const config = Config.getConfig(); @@ -48,19 +50,6 @@ export function setCacheAllowed(_rq: express.Request, rs: express.Response, nxt: nxt(); } -export function generateRandomString(length: number) { - const characters = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - let randomString = ""; - - for (let i = 0; i < length; i++) { - const randomIndex = Math.floor(Math.random() * characters.length); - randomString += characters.charAt(randomIndex); - } - - return randomString; -} - export function checkQueryTypes(typeDef: T) { return ( rq: express.Request, @@ -251,31 +240,6 @@ export class RateLimiter { } } -export interface TokenBaseFormat { - typ: AuthType; - iss: string; - exp: number; -} -export type TokenFormat = UserTokenFormat | ProfileTokenFormat; - -const TokenBaseSchema = z.object({ - typ: z.nativeEnum(AuthType), - iss: z.string().url(), - exp: z.number() -}); -export const UserTokenSchema = TokenBaseSchema.extend({ - sub: z.string(), - typ: z.literal(AuthType.Web) -}); -export const ProfileTokenSchema = TokenBaseSchema.extend({ - sub: z.number(), - typ: z.literal(AuthType.Game) -}); -export const TokenSchema = z.discriminatedUnion('typ', [ - UserTokenSchema, - ProfileTokenSchema -]); - export async function Authentication( rq: express.Request, rs: express.Response, @@ -375,4 +339,12 @@ export function stopTimer(_rq: express.Request, rs: express.Response) { log.n(`(${rs.locals.reqId}) Middleware took ${(performance.now() - rs.locals.timer).toString().substring(0, 6)} ms`); } +export function requestDebug(rq: express.Request, _rs: express.Response, nxt: express.NextFunction) { + log.d(`URL: ${rq.originalUrl}`); + log.d(`From IP: ${getSrcIpDefault(rq)}`); + log.d(`Headers: ${Object.keys(rq.headers).map(val => `\n ${val}: ${rq.headers[val]}`)}`); + + nxt(); +} + export * as APIUtils from "./apiutils.ts"; diff --git a/src/config/GalvanicConfiguration.ts b/src/config/GalvanicConfiguration.ts new file mode 100644 index 0000000..9051308 --- /dev/null +++ b/src/config/GalvanicConfiguration.ts @@ -0,0 +1,73 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import type { PhotonRegionCodeNumber, PhotonRegionCodeString } from "../data/live/PhotonTypes.ts"; + +type RedisConfiguration = { + host: string; + port: number; + username: string; + password: string; + db: number; +}; +type WebConfiguration = { + port: number; + host: string; + publichost: string; + securepublichost: boolean; +}; +type WebRootConfiguration = { + api: WebConfiguration; + socket: WebConfiguration; +}; +type PublicConfiguration = { + serverName: string; + serverId: string; + owner: string; + motd: string; + levelScale: number; + maxLevels: number; + patches: string[]; + photonRegionId: PhotonRegionCodeNumber | PhotonRegionCodeString; + initialRoom: string | null; +}; +type LoggingConfiguration = { + notfound: boolean; + debug: boolean; + network: boolean; +}; +type AuthConfiguration = { + secret: string; + /** + * In Hours + */ + timeout: number; + steamkey: string | null; +}; +type GeneralConfiguration = { + /** In milliseconds */ + watchdogTimeout: number; +} + +export type GalvanicConfiguration = { + redis: RedisConfiguration; + web: WebRootConfiguration; + public: PublicConfiguration; + general: GeneralConfiguration; + logging: LoggingConfiguration; + auth: AuthConfiguration; +}; \ No newline at end of file diff --git a/src/config.ts b/src/config/config.ts similarity index 66% rename from src/config.ts rename to src/config/config.ts index c192a22..4dc99a0 100644 --- a/src/config.ts +++ b/src/config/config.ts @@ -17,72 +17,11 @@ along with this program. If not, see . */ import Logging from "@proxnet/undead-logging"; import * as fs from "node:fs"; -import { PhotonRegionCodeNumber, PhotonRegionCodeString } from "./data/live/types.ts"; +import { GalvanicConfiguration } from "./GalvanicConfiguration.ts"; +import { PhotonRegionCodeNumber } from "../data/live/PhotonTypes.ts"; const log = new Logging("Config"); -type RedisConfiguration = { - host: string; - port: number; - username: string; - password: string; - db: number; -}; - -type WebConfiguration = { - port: number; - host: string; - publichost: string; - securepublichost: boolean; -} - -type WebRootConfiguration = { - api: WebConfiguration, - socket: WebConfiguration -}; - -type PublicConfiguration = { - serverName: string; - serverId: string; - owner: string; - motd: string; - levelScale: number; - maxLevels: number; - patches: string[]; - photonRegionId: PhotonRegionCodeString | PhotonRegionCodeNumber; - initialRoom: string | null; -}; - -type LoggingConfiguration = { - notfound: boolean; - debug: boolean; - network: boolean; -}; - -type DiscordConfiguration = { - token: string; - clientId: string; - guildId: string; -}; - -type AuthConfiguration = { - secret: string; - /** - * In Hours - */ - timeout: number; - steamkey: string | null; -}; - -export type GalvanicConfiguration = { - redis: RedisConfiguration; - web: WebRootConfiguration; - public: PublicConfiguration; - logging: LoggingConfiguration; - discord: DiscordConfiguration | null; - auth: AuthConfiguration; -}; - export const defaultConfig: GalvanicConfiguration = { redis: { host: "127.0.0.1", @@ -116,12 +55,14 @@ export const defaultConfig: GalvanicConfiguration = { photonRegionId: PhotonRegionCodeNumber.us, initialRoom: null }, + general: { + watchdogTimeout: 60000 + }, logging: { notfound: false, debug: false, network: false, }, - discord: null, auth: { secret: "CHANGE-ME-PLEASE", timeout: 3, @@ -140,12 +81,12 @@ try { } /** Does the configuration file exist on the disk? */ -export function configurationExists() { +function configurationExists() { return fs.existsSync("./config.json"); } /** Place [or overwrite] the [existing] default configuration in the current directory */ -export function generateDefaultConfig() { +function generateDefaultConfig() { fs.writeFileSync( "./config.json", JSON.stringify(defaultConfig, undefined, " "), diff --git a/src/data/profile/base/events.ts b/src/data/UserTypes.ts similarity index 88% rename from src/data/profile/base/events.ts rename to src/data/UserTypes.ts index 45cdee2..9fc2b3f 100644 --- a/src/data/profile/base/events.ts +++ b/src/data/UserTypes.ts @@ -15,8 +15,12 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -export class ProfileEventsManager { - - +export type UserInitOptions = { + client_id: string; + pubkey: string; +}; +export enum AuthType { + Game, + Web } \ No newline at end of file diff --git a/src/data/auth/TokenBaseFormat.ts b/src/data/auth/TokenBaseFormat.ts new file mode 100644 index 0000000..309ed08 --- /dev/null +++ b/src/data/auth/TokenBaseFormat.ts @@ -0,0 +1,43 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import { AuthType } from "../UserTypes.ts"; + +export type TokenFormat = UserTokenFormat | ProfileTokenFormat; + +export interface TokenBaseFormat { + typ: AuthType; + iss: string; + exp: number; +} + +export interface TokenBaseFormat { + typ: AuthType; + iss: string; + exp: number; +} + +export interface UserTokenFormat extends TokenBaseFormat { + sub: string; + typ: AuthType.Web; +} + +export interface ProfileTokenFormat extends TokenBaseFormat { + sub: number; + role: "developer" | "user"; + typ: AuthType.Game; +} \ No newline at end of file diff --git a/src/data/auth/TokenSchema.ts b/src/data/auth/TokenSchema.ts new file mode 100644 index 0000000..d482198 --- /dev/null +++ b/src/data/auth/TokenSchema.ts @@ -0,0 +1,37 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import z from "zod"; +import { AuthType } from "../UserTypes.ts"; + +const TokenBaseSchema = z.object({ + typ: z.nativeEnum(AuthType), + iss: z.string().url(), + exp: z.number() +}); +export const UserTokenSchema = TokenBaseSchema.extend({ + sub: z.string(), + typ: z.literal(AuthType.Web) +}); +export const ProfileTokenSchema = TokenBaseSchema.extend({ + sub: z.number(), + typ: z.literal(AuthType.Game) +}); +export const TokenSchema = z.discriminatedUnion('typ', [ + UserTokenSchema, + ProfileTokenSchema +]); \ No newline at end of file diff --git a/src/data/baseevent.ts b/src/data/baseevent.ts index 531ca3d..bbce790 100644 --- a/src/data/baseevent.ts +++ b/src/data/baseevent.ts @@ -15,51 +15,26 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import Logging from "@proxnet/undead-logging"; +type Callback = (event: T) => void; -const log = new Logging("BaseEvent"); +export class EventManager { + #listeners: { + [K in keyof Events]?: Set> + } = {}; -export interface Event { - time: Date -} - -export class EventManager { - - private eventCallbacks: Map void>> = new Map(); - - private getSubSet(event: string): Set<(ev: unknown) => void> { - let subset = this.eventCallbacks.get(event); - if (!subset) { - subset = new Set(); - this.eventCallbacks.set(event, subset); - } - return subset; + on(eventName: K, callback: Callback): void { + if (!this.#listeners[eventName]) + this.#listeners[eventName] = new Set(); + this.#listeners[eventName]!.add(callback); } - - on(event: string, cb: (ev: T) => void) { - const typeSafeCallback = ((ev: unknown) => { - cb(ev as T); - }); - - this.getSubSet(event).add(typeSafeCallback); - return typeSafeCallback; + off(eventName: K, callback: Callback): void { + this.#listeners[eventName]?.delete(callback); + if (this.#listeners[eventName]?.size === 0) + delete this.#listeners[eventName]; } - - off(event: string, cb: (ev: unknown) => void) { - const subset = this.getSubSet(event); - subset.delete(cb); - } - - emit(event: string, ev: T) { - const subset = this.getSubSet(event); - for (const cb of subset.values()) { - try { - cb(ev); - } catch (err) { - if (err instanceof Error) log.e(`Error when executing callback: ${err.stack}`); - else log.e(`Error when executing callback: ${err}`); - } - } + + emit(eventName: K, payload: Events[K]): void { + this.#listeners[eventName]?.forEach((callback) => callback(payload)); } } \ No newline at end of file diff --git a/src/data/config.ts b/src/data/config.ts deleted file mode 100644 index 4d80935..0000000 --- a/src/data/config.ts +++ /dev/null @@ -1,164 +0,0 @@ -/* Galvanic Corrosion - Rec Room custom server for communities. - -Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . */ - -import { Config } from "../config.ts"; -import { Redis } from "../db.ts"; -import { Objectives, ObjectiveType } from "./objectives.ts"; - -export type LevelProgressionItem = { - Level: number; - RequiredXp: number; -}; -export type AutoMicMutingConfig = { - MicSpamVolumeThreshold: number, - MicVolumeSampleInterval: number, - MicVolumeSampleRollingWindowLength: number, - MicSpamSamplePercentageForWarning: number, - MicSpamSamplePercentageForWarningToEnd: number, - MicSpamSamplePercentageForForceMute: number, - MicSpamSamplePercentageForForceMuteToEnd: number, - MicSpamWarningStateVolumeMultiplier: number -} -export type PublicConfig = { - ShareBaseUrl: string - ServerMaintenance: { - StartsInMinutes: number; - }; - LevelProgressionMaps: LevelProgressionItem[]; - DailyObjectives: Objectives.Objective[][]; - AutoMicMutingConfig: AutoMicMutingConfig, -}; - -/** - * Plain public config, NOT GameConfigs - */ -export function getConfig() { - const c = Config.getConfig(); - if (typeof c == "undefined") return null; - const config = c as Config.GalvanicConfiguration; - - function generateLevelProgressionMap() { - const m: LevelProgressionItem[] = []; - for (let i = 0; i < config.public.maxLevels + 1; i++) { - m.push({ - Level: i, - RequiredXp: Math.round(i * config.public.levelScale * 20), - }); - } - return m; - } - - const conf: PublicConfig = { - ServerMaintenance: { - StartsInMinutes: 0, - }, - LevelProgressionMaps: generateLevelProgressionMap(), - DailyObjectives: [ - [ - {type: ObjectiveType.Default, score: 0}, - {type: ObjectiveType.Default, score: 0}, - {type: ObjectiveType.Default, score: 0} - ], - [ - {type: ObjectiveType.Default, score: 0}, - {type: ObjectiveType.Default, score: 0}, - {type: ObjectiveType.Default, score: 0} - ], - [ - {type: ObjectiveType.Default, score: 0}, - {type: ObjectiveType.Default, score: 0}, - {type: ObjectiveType.Default, score: 0} - ], - [ - {type: ObjectiveType.Default, score: 0}, - {type: ObjectiveType.Default, score: 0}, - {type: ObjectiveType.Default, score: 0} - ], - [ - {type: ObjectiveType.Default, score: 0}, - {type: ObjectiveType.Default, score: 0}, - {type: ObjectiveType.Default, score: 0} - ], - [ - {type: ObjectiveType.Default, score: 0}, - {type: ObjectiveType.Default, score: 0}, - {type: ObjectiveType.Default, score: 0} - ], - [ - {type: ObjectiveType.Default, score: 0}, - {type: ObjectiveType.Default, score: 0}, - {type: ObjectiveType.Default, score: 0} - ] - ], - AutoMicMutingConfig: { - MicSpamVolumeThreshold: 1.125, - MicVolumeSampleInterval: 0.25, - MicVolumeSampleRollingWindowLength: 7.0, - MicSpamSamplePercentageForWarning: 0.8, - MicSpamSamplePercentageForWarningToEnd: 0.2, - MicSpamSamplePercentageForForceMute: 0.8, - MicSpamSamplePercentageForForceMuteToEnd: 0.2, - MicSpamWarningStateVolumeMultiplier: 0.25 - }, - ShareBaseUrl: `${config.web.api.securepublichost ? 'https' : 'http'}://${config.web.api.publichost}/{0}` // {0} is replaced by the game - }; - - return conf; -} - -export async function getAllGameConfigs() { - try { - const gameConfigs = new Map(); - const val = await Redis.Database.hgetall( - Redis.buildKey( - Redis.KeyGroups.Config.Root, - Redis.KeyGroups.Config.Game, - ), - ); - - for (const key of Object.keys(val)) { - gameConfigs.set(key, val[key]); - } - - return gameConfigs; - } catch (error) { - console.error("Error fetching game configs:", error); - throw error; - } -} - -export function setGameConfig(key: string, value: string) { - return Redis.Database.hset( - Redis.buildKey( - Redis.KeyGroups.Config.Root, - Redis.KeyGroups.Config.Game, - ), - key, - value, - ); -} -export function getGameConfig(key: string) { - return Redis.Database.hget( - Redis.buildKey( - Redis.KeyGroups.Config.Root, - Redis.KeyGroups.Config.Game, - ), - key, - ); -} - -export * as GameConfigs from "./config.ts"; diff --git a/src/data/config/GameConfigs.ts b/src/data/config/GameConfigs.ts new file mode 100644 index 0000000..6097621 --- /dev/null +++ b/src/data/config/GameConfigs.ts @@ -0,0 +1,61 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import { Redis } from "../../db.ts"; + +export async function getAllGameConfigs() { + try { + const gameConfigs = new Map(); + const val = await Redis.Database.hgetall( + Redis.buildKey( + Redis.KeyGroups.Config.Root, + Redis.KeyGroups.Config.Game, + ), + ); + + for (const key of Object.keys(val)) { + gameConfigs.set(key, val[key]); + } + + return gameConfigs; + } catch (error) { + console.error("Error fetching game configs:", error); + throw error; + } +} + +export function setGameConfig(key: string, value: string) { + return Redis.Database.hset( + Redis.buildKey( + Redis.KeyGroups.Config.Root, + Redis.KeyGroups.Config.Game, + ), + key, + value, + ); +} +export function getGameConfig(key: string) { + return Redis.Database.hget( + Redis.buildKey( + Redis.KeyGroups.Config.Root, + Redis.KeyGroups.Config.Game, + ), + key, + ); +} + +export * as GameConfigs from "./GameConfigs.ts"; diff --git a/src/data/config/PublicConfig.ts b/src/data/config/PublicConfig.ts new file mode 100644 index 0000000..05eeb29 --- /dev/null +++ b/src/data/config/PublicConfig.ts @@ -0,0 +1,95 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import { Config } from "../../config/config.ts"; +import type { GalvanicConfiguration } from "../../config/GalvanicConfiguration.ts"; +import { ObjectiveType } from "../content/ObjectiveTypes.ts"; +import { PublicConfig, LevelProgressionItem } from "./PublicConfigTypes.ts"; + +export function getPublicConfig() { + const c = Config.getConfig(); + if (typeof c == "undefined") return null; + const config = c as GalvanicConfiguration; + + function generateLevelProgressionMap() { + const m: LevelProgressionItem[] = []; + for (let i = 0; i < config.public.maxLevels + 1; i++) { + m.push({ + Level: i, + RequiredXp: Math.round(i * config.public.levelScale * 20), + }); + } + return m; + } + + const conf: PublicConfig = { + ServerMaintenance: { + StartsInMinutes: 0, + }, + LevelProgressionMaps: generateLevelProgressionMap(), + DailyObjectives: [ + [ + { type: ObjectiveType.Default, score: 0 }, + { type: ObjectiveType.Default, score: 0 }, + { type: ObjectiveType.Default, score: 0 } + ], + [ + { type: ObjectiveType.Default, score: 0 }, + { type: ObjectiveType.Default, score: 0 }, + { type: ObjectiveType.Default, score: 0 } + ], + [ + { type: ObjectiveType.Default, score: 0 }, + { type: ObjectiveType.Default, score: 0 }, + { type: ObjectiveType.Default, score: 0 } + ], + [ + { type: ObjectiveType.Default, score: 0 }, + { type: ObjectiveType.Default, score: 0 }, + { type: ObjectiveType.Default, score: 0 } + ], + [ + { type: ObjectiveType.Default, score: 0 }, + { type: ObjectiveType.Default, score: 0 }, + { type: ObjectiveType.Default, score: 0 } + ], + [ + { type: ObjectiveType.Default, score: 0 }, + { type: ObjectiveType.Default, score: 0 }, + { type: ObjectiveType.Default, score: 0 } + ], + [ + { type: ObjectiveType.Default, score: 0 }, + { type: ObjectiveType.Default, score: 0 }, + { type: ObjectiveType.Default, score: 0 } + ] + ], + AutoMicMutingConfig: { + MicSpamVolumeThreshold: 1.125, + MicVolumeSampleInterval: 0.25, + MicVolumeSampleRollingWindowLength: 7.0, + MicSpamSamplePercentageForWarning: 0.8, + MicSpamSamplePercentageForWarningToEnd: 0.2, + MicSpamSamplePercentageForForceMute: 0.8, + MicSpamSamplePercentageForForceMuteToEnd: 0.2, + MicSpamWarningStateVolumeMultiplier: 0.25 + }, + ShareBaseUrl: `${config.web.api.securepublichost ? 'https' : 'http'}://${config.web.api.publichost}/{0}` // {0} is replaced by the game + }; + + return conf; +} \ No newline at end of file diff --git a/src/data/config/PublicConfigTypes.ts b/src/data/config/PublicConfigTypes.ts new file mode 100644 index 0000000..35bcc8a --- /dev/null +++ b/src/data/config/PublicConfigTypes.ts @@ -0,0 +1,43 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import type { Objective } from "../content/ObjectiveTypes.ts"; + +export interface LevelProgressionItem { + Level: number; + RequiredXp: number; +}; +interface AutoMicMutingConfig { + MicSpamVolumeThreshold: number; + MicVolumeSampleInterval: number; + MicVolumeSampleRollingWindowLength: number; + MicSpamSamplePercentageForWarning: number; + MicSpamSamplePercentageForWarningToEnd: number; + MicSpamSamplePercentageForForceMute: number; + MicSpamSamplePercentageForForceMuteToEnd: number; + MicSpamWarningStateVolumeMultiplier: number; +}; + +export type PublicConfig = { + ShareBaseUrl: string; + ServerMaintenance: { + StartsInMinutes: number; + }; + LevelProgressionMaps: LevelProgressionItem[]; + DailyObjectives: Objective[][]; + AutoMicMutingConfig: AutoMicMutingConfig; +}; \ No newline at end of file diff --git a/src/data/objectives.ts b/src/data/content/ObjectiveTypes.ts similarity index 98% rename from src/data/objectives.ts rename to src/data/content/ObjectiveTypes.ts index 0aea0e2..794c30d 100644 --- a/src/data/objectives.ts +++ b/src/data/content/ObjectiveTypes.ts @@ -108,6 +108,4 @@ export enum ObjectiveType { export type Objective = { type: ObjectiveType; score: number; -}; - -export * as Objectives from "./objectives.ts"; +} diff --git a/src/data/content/cdn.ts b/src/data/content/cdn.ts index a7a7679..b930c21 100644 --- a/src/data/content/cdn.ts +++ b/src/data/content/cdn.ts @@ -16,10 +16,10 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import Logging from "@proxnet/undead-logging"; -import { generateRandomString } from "../../apiutils.ts"; -import { Profile } from "../profiles.ts"; +import { generateRandomString } from "../../utils.ts"; +import type { Profile } from "../profile/base/profiles.ts"; import * as fs from "node:fs"; -import Server from "../server.ts"; +import Server from "../server/server.ts"; const log = new Logging("CDN"); @@ -68,7 +68,6 @@ export class CDNBase { pathParts.pop(); const dirPath = pathParts.join('/'); - log.d(dirPath); if (dirPath) await Deno.mkdir(dirPath, { recursive: true }); } @@ -113,6 +112,7 @@ export class CDNBase { const metaData = await Deno.readTextFile(`${path}.gcmeta`); const parsedMeta = JSON.parse(metaData); + const meta: MetaFile = { creationPlayer: Server.UnifiedProfile.get(parsedMeta.creationPlayer) || undefined, dateCreated: new Date(parsedMeta.dateCreated), diff --git a/src/data/server.ts b/src/data/content/rooms/RoomEvents.ts similarity index 72% rename from src/data/server.ts rename to src/data/content/rooms/RoomEvents.ts index a6d5a75..bcdf46f 100644 --- a/src/data/server.ts +++ b/src/data/content/rooms/RoomEvents.ts @@ -15,14 +15,13 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { EventManager } from "./baseevent.ts"; -import { CDNBase } from "./content/cdn.ts"; -import { UnifiedProfileBase } from "./profiles.ts"; +import { RoomFactory } from "./RoomFactory.ts"; +import { SubroomFactory } from "./SubroomFactory.ts"; -class ServerBase extends EventManager { - CDN = new CDNBase(); - UnifiedProfile = new UnifiedProfileBase(); +export interface SubroomUpdatedEvent { + subroom: SubroomFactory } -const Server = new ServerBase(); -export default Server; +export interface RoomUpdatedEvent { + room: RoomFactory +} \ No newline at end of file diff --git a/src/data/content/rooms/RoomFactory.ts b/src/data/content/rooms/RoomFactory.ts index 99c0444..5e065e7 100644 --- a/src/data/content/rooms/RoomFactory.ts +++ b/src/data/content/rooms/RoomFactory.ts @@ -15,11 +15,16 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ +import Logging from "@proxnet/undead-logging"; import { Redis } from "../../../db.ts"; -import Rooms from "../rooms.ts"; -import { FactoryMode, HardwareSupport, HardwareSupportStrings, RoomAccessibility, RoomDataTypes, RoomDetails, RoomState, WriteMode } from "./DataTypes.ts"; +import Server from "../../server/server.ts"; +import { RoomDataTypes } from "./base/DataTypes.ts"; import { SubroomFactory } from "./SubroomFactory.ts"; +export const roomdebug = false; + +const log = new Logging("RoomFactory"); + interface RoomFactoryOptions { id?: number; name?: string; @@ -29,6 +34,14 @@ interface RoomFactoryOptions { export class RoomFactory { + static async getIdFromName(name: string) { + const unparsedId = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Room_Names, name)); + if (!unparsedId) return null; + const parsedId = parseInt(unparsedId); + if (isNaN(parsedId)) return null; + return parsedId; + } + static Keys = { Meta: "roommeta", Subrooms: "subrooms", @@ -68,7 +81,7 @@ export class RoomFactory { async init() { - if (this.factoryMode !== FactoryMode.Fetch) { + if (this.factoryMode !== RoomDataTypes.FactoryMode.Fetch) { if (!this.#specifiedId) throw this.#mustSpecifyIdInWriteModeError; this.#resolvedId = this.#specifiedId; return this; @@ -76,7 +89,7 @@ export class RoomFactory { if (!this.#specifiedId) { if (!this.#specifiedName) throw this.#mustSpecifyEitherIdOrNameError; - const id = await Rooms.getIdFromName(this.#specifiedName); + const id = await RoomFactory.getIdFromName(this.#specifiedName); if (!id) return null; this.#specifiedId = id; } @@ -89,6 +102,8 @@ export class RoomFactory { RoomFactory.Keys.Meta )); + if (roomdebug) log.d(`Init success, specifiedId: ${this.#specifiedId}`); + return this; } @@ -119,6 +134,8 @@ export class RoomFactory { } if (this.Name !== 'DormRoom') await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Room_Names, this.Name), this.RoomId); + + Server.emit('room.updated', { room: this }); } async export() { @@ -128,7 +145,7 @@ export class RoomFactory { const subroomPromises = subroomIds.map(id => this.getSubroom(id).init()); const subrooms = (await Promise.all(subroomPromises)).map(subroom => subroom.export()); - const details: RoomDetails = { + const details: RoomDataTypes.RoomDetails = { Room: { RoomId: this.RoomId, Name: this.Name, @@ -160,13 +177,13 @@ export class RoomFactory { Tags: [] } + if (roomdebug) log.d(`Exported details for room ${this.RoomId}`); return details; } - getSubroom(id: number, factoryMode?: FactoryMode, writeMode?: WriteMode) { + getSubroom(id: number, factoryMode?: RoomDataTypes.FactoryMode, writeMode?: RoomDataTypes.WriteMode) { if (!this.#resolvedId) throw this.#unresolvedError; return new SubroomFactory({ - roomId: this.#resolvedId, subroomId: id, factoryMode: factoryMode ? factoryMode : undefined, writeMode : writeMode ? writeMode : undefined @@ -240,11 +257,11 @@ export class RoomFactory { set ImageName(data) { this.#setHashValue(this.#imageKey, data) } #stateKey = 'State'; - get State(): RoomState { return this.#fetchNumberKey(this.#stateKey, RoomState.Active) } + get State(): RoomDataTypes.RoomState { return this.#fetchNumberKey(this.#stateKey, RoomDataTypes.RoomState.Active) } set State(data) { this.#setHashValue(this.#stateKey, data) } #accessKey = 'RoomAccessibility'; - get RoomAccessibility(): RoomAccessibility { return this.#fetchNumberKey(this.#accessKey, RoomAccessibility.Unlisted) } + get RoomAccessibility(): RoomDataTypes.RoomAccessibility { return this.#fetchNumberKey(this.#accessKey, RoomDataTypes.RoomAccessibility.Unlisted) } set RoomAccessibility(data) { this.#setHashValue(this.#accessKey, data) } #votingKey = 'SupportsLevelVoting'; @@ -279,20 +296,20 @@ export class RoomFactory { get DisableMicAutoMute() { return this.#fetchBooleanKey(this.#muteKey, false) } set DisableMicAutoMute(data) { this.#setHashValue(this.#muteKey, data) } - async getHardwareSupport(): Promise { + async getHardwareSupport(): Promise { if (!this.#resolvedId) throw this.#unresolvedError; return (await Redis.Database.smembers(Redis.buildKey( Redis.KeyGroups.Rooms.Root, this.#resolvedId.toString(), RoomFactory.Keys.HardwareSupport - ))) as HardwareSupport[]; + ))) as RoomDataTypes.HardwareSupport[]; } - async addHardwareSupport(hardware: HardwareSupport | HardwareSupport[] | '*') { + async addHardwareSupport(hardware: RoomDataTypes.HardwareSupport | RoomDataTypes.HardwareSupport[] | '*') { if (!this.#resolvedId) throw this.#unresolvedError; if (hardware === '*') { - await Promise.all(HardwareSupportStrings.map(str => this.addHardwareSupport(str as HardwareSupport) )); + await Promise.all(RoomDataTypes.HardwareSupportStrings.map(str => this.addHardwareSupport(str as RoomDataTypes.HardwareSupport) )); return; } @@ -314,7 +331,7 @@ export class RoomFactory { } } - async removeHardwareSupport(hardware: HardwareSupport) { + async removeHardwareSupport(hardware: RoomDataTypes.HardwareSupport) { if (!this.#resolvedId) throw this.#unresolvedError; await Redis.Database.srem(Redis.buildKey( Redis.KeyGroups.Rooms.Root, diff --git a/src/data/content/rooms.ts b/src/data/content/rooms/Rooms.ts similarity index 66% rename from src/data/content/rooms.ts rename to src/data/content/rooms/Rooms.ts index 624dd37..148daff 100644 --- a/src/data/content/rooms.ts +++ b/src/data/content/rooms/Rooms.ts @@ -15,21 +15,27 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { Redis } from "../../db.ts"; -import { Profile } from "../profiles.ts"; +import { Redis } from "../../../db.ts"; +import { Profile } from "../../profile/base/profiles.ts"; import Logging from "@proxnet/undead-logging"; -import { BuiltinRoom, FactoryMode, IntegratedRoomScene, RoomAccessibility, RoomDataTypes, RoomDetails, RoomState, WriteMode } from "./rooms/DataTypes.ts"; -import { RoomFactory } from "./rooms/RoomFactory.ts"; -import { SubroomFactory } from "./rooms/SubroomFactory.ts"; -import { RootPath } from "../../path.ts"; -import { Instance } from "../live/instances.ts"; -import { PushNotificationId } from "../../socket/types.ts"; +import { SubroomFactory } from "./SubroomFactory.ts"; +import { RootPath } from "../../../path.ts"; +import { RoomFactory } from "./RoomFactory.ts"; +import { RoomDataTypes } from "../rooms/base/DataTypes.ts"; +import Rooms from "./base/RoomsBase.ts"; const log = new Logging("Rooms"); -const rooms = JSON.parse(Deno.readTextFileSync(`${RootPath}/res/rooms.json`)) as BuiltinRoom[]; +const builtinRooms = JSON.parse(Deno.readTextFileSync(`${RootPath}/res/rooms.json`)) as RoomDataTypes.BuiltinRoom[]; -class RoomsBase { +const baseImageChanges = [ + { room: "DodgeballVR", image: "Dodgeball" }, + { room: "PaintballVR", image: "Paintball" }, + { room: "StuntRunnerBaseRoom", image: "StuntRunner" }, + { room: "BowlingAlley", image: "Bowling" }, +]; + +class RoomsMiscBase { static Keys = { BuiltinGenerated: "builtinrooms-done", @@ -37,33 +43,13 @@ class RoomsBase { } getAllBuiltinRooms() { - return rooms; - } - - async get(id: number) { - try { - const factory = await new RoomFactory({ id: id }).init(); - if (!factory) return null; - return factory.export(); - } catch { - return null; - } - } - - async getByName(name: string) { - try { - const factory = await new RoomFactory({ name: name }).init(); - if (!factory) return null; - return factory.export(); - } catch { - return null; - } + return builtinRooms; } async getAllBuiltinRoomGenerations() { - const ids = await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.AGRooms)); + const ids = await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsMiscBase.Keys.AGRooms)); const parsedIds = ids.map(val => parseInt(val)).filter(val => !isNaN(val)); - return (await Promise.all(parsedIds.map(id => this.get(id)))).filter(val => val !== null); + return (await Promise.all(parsedIds.map(id => Rooms.get(id)))).filter(val => val !== null); } async #getAvailableRoomId() { @@ -77,9 +63,7 @@ class RoomsBase { let id = Math.round(Math.random() * Math.pow(2, 31)); while ((await Redis.Database.exists( Redis.buildKey( - Redis.KeyGroups.Rooms.Root, - roomid.toString(), - RoomFactory.Keys.Subrooms, + Redis.KeyGroups.Subrooms.Root, id.toString(), SubroomFactory.Keys.Meta ))) >= 1) @@ -93,20 +77,20 @@ class RoomsBase { result: RoomDataTypes.CreateModifyRoomStatus; } - const factory = await new RoomFactory({ id: roomid, factoryMode: FactoryMode.Fetch }).init(); + const factory = await new RoomFactory({ id: roomid, factoryMode: RoomDataTypes.FactoryMode.Fetch }).init(); if (!factory || !factory.CloningAllowed) return { result: RoomDataTypes.CreateModifyRoomStatus.PermissionDenied } as RoomClone; if (factory.Name == 'DormRoom') return { result: RoomDataTypes.CreateModifyRoomStatus.ReservedName } as RoomClone; if (factory.Name == newname) return { result: RoomDataTypes.CreateModifyRoomStatus.DuplicateName } as RoomClone; - const newFactory = await new RoomFactory({ id: await Rooms.#getAvailableRoomId(), factoryMode: FactoryMode.Write }).init(); + const newFactory = await new RoomFactory({ id: await RoomsMisc.#getAvailableRoomId(), factoryMode: RoomDataTypes.FactoryMode.Write }).init(); if (!newFactory) return { result: RoomDataTypes.CreateModifyRoomStatus.Unknown } as RoomClone; newFactory.CreatorPlayerId = newowner.getId(); newFactory.Description = factory.Description; newFactory.Name = newname; newFactory.ImageName = factory.ImageName; - newFactory.State = RoomState.Active; - newFactory.RoomAccessibility = RoomAccessibility.Private; + newFactory.State = RoomDataTypes.RoomState.Active; + newFactory.RoomAccessibility = RoomDataTypes.RoomDataTypes.RoomAccessibility.Private; newFactory.SupportsLevelVoting = factory.SupportsLevelVoting; newFactory.IsAGRoom = false; newFactory.IsDormRoom = factory.IsDormRoom; @@ -122,9 +106,10 @@ class RoomsBase { const oldSubroomIds = await factory.getAllSubroomIds(); const promises = oldSubroomIds.map(async (id) => { - const newSubroomFactory = await newFactory.getSubroom(id, FactoryMode.Write, WriteMode.Overwrite).init(); - const oldSubroomFactory = await factory.getSubroom(id, FactoryMode.Fetch).init(); + const newSubroomFactory = await newFactory.getSubroom(id, RoomDataTypes.RoomDataTypes.FactoryMode.Write, RoomDataTypes.RoomDataTypes.WriteMode.Overwrite).init(); + const oldSubroomFactory = await factory.getSubroom(id, RoomDataTypes.RoomDataTypes.FactoryMode.Fetch).init(); + newSubroomFactory.RoomId = newFactory.RoomId; newSubroomFactory.RoomSceneLocationId = oldSubroomFactory.RoomSceneLocationId; newSubroomFactory.Name = oldSubroomFactory.Name; newSubroomFactory.IsSandbox = oldSubroomFactory.IsSandbox; @@ -137,7 +122,7 @@ class RoomsBase { await Promise.all(promises); await newFactory.write(); - newFactory.factoryMode = FactoryMode.Fetch; + newFactory.factoryMode = RoomDataTypes.RoomDataTypes.FactoryMode.Fetch; return { factory: newFactory, @@ -176,15 +161,15 @@ class RoomsBase { async generateNewDorm(player: Profile) { const id = await this.#getAvailableRoomId(); - const factory = await new RoomFactory({ id: id, factoryMode: FactoryMode.Write, writeMode: WriteMode.WriteIfFree }).init(); + const factory = await new RoomFactory({ id: id, factoryMode: RoomDataTypes.FactoryMode.Write, writeMode: RoomDataTypes.WriteMode.WriteIfFree }).init(); if (!factory) return null; factory.Name = "DormRoom"; factory.Description = "Your private room."; factory.CreatorPlayerId = player.getId(); factory.ImageName = "DefaultProfileImage.png"; - factory.State = RoomState.Active; - factory.RoomAccessibility = RoomAccessibility.Private; + factory.State = RoomDataTypes.RoomState.Active; + factory.RoomAccessibility = RoomDataTypes.RoomAccessibility.Private; factory.SupportsLevelVoting = false; factory.IsAGRoom = false; factory.IsDormRoom = true; @@ -195,10 +180,11 @@ class RoomsBase { factory.addHardwareSupport('*'); - const subroomFactory = await factory.getSubroom(await this.#getAvailableSubRoomId(id), FactoryMode.Write, WriteMode.WriteIfFree).init(); + const subroomFactory = await factory.getSubroom(await this.#getAvailableSubRoomId(id), RoomDataTypes.FactoryMode.Write, RoomDataTypes.WriteMode.WriteIfFree).init(); if (!subroomFactory) return null; - subroomFactory.RoomSceneLocationId = IntegratedRoomScene.DormRoom; + subroomFactory.RoomId = id; + subroomFactory.RoomSceneLocationId = RoomDataTypes.IntegratedRoomScene.DormRoom; subroomFactory.Name = "Home"; subroomFactory.IsSandbox = true; subroomFactory.DataBlobName = ""; @@ -212,26 +198,20 @@ class RoomsBase { } async generateBuiltinRooms() { - - if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.BuiltinGenerated))) !== null) return true; - await Promise.all(rooms.map(async builtinRoom => { + if ((await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsMiscBase.Keys.BuiltinGenerated))) !== null) return true; + + await Promise.all(builtinRooms.map(async builtinRoom => { if (builtinRoom.Name == 'DormRoom') return; const newId = await this.#getAvailableRoomId(); - await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.AGRooms), newId); + await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsMiscBase.Keys.AGRooms), newId); - const factory = await new RoomFactory({ id: newId, factoryMode: FactoryMode.Write, writeMode: WriteMode.Overwrite }).init(); + const factory = await new RoomFactory({ id: newId, factoryMode: RoomDataTypes.FactoryMode.Write, writeMode: RoomDataTypes.WriteMode.Overwrite }).init(); if (!factory) return; factory.Name = builtinRoom.Name; factory.Description = builtinRoom.Description; factory.CreatorPlayerId = 1; - const baseImageChanges = [ - { room: "DodgeballVR", image: "Dodgeball" }, - { room: "PaintballVR", image: "Paintball" }, - { room: "StuntRunnerBaseRoom", image: "StuntRunner" }, - { room: "BowlingAlley", image: "Bowling" }, - ] if (baseImageChanges.find(change => change.room == builtinRoom.Name)) { const image = baseImageChanges.find(change => change.room == builtinRoom.Name)!; @@ -239,7 +219,7 @@ class RoomsBase { } else factory.ImageName = `${builtinRoom.Name}.png`; - factory.State = RoomState.Active; + factory.State = RoomDataTypes.RoomState.Active; factory.RoomAccessibility = builtinRoom.Accessibility; factory.SupportsLevelVoting = builtinRoom.SupportsLevelVoting; factory.IsAGRoom = true; @@ -254,9 +234,10 @@ class RoomsBase { await Promise.all(builtinRoom.Scenes.map(async subroom => { const newSubroomId = await this.#getAvailableSubRoomId(newId); - const subroomFactory = await factory.getSubroom(newSubroomId, FactoryMode.Write, WriteMode.Overwrite).init(); + const subroomFactory = await factory.getSubroom(newSubroomId, RoomDataTypes.FactoryMode.Write, RoomDataTypes.WriteMode.Overwrite).init(); if (!subroomFactory) return; + subroomFactory.RoomId = newId; subroomFactory.RoomSceneLocationId = subroom.RoomSceneLocationId; subroomFactory.Name = subroom.Name; subroomFactory.IsSandbox = subroom.IsSandbox; @@ -269,53 +250,13 @@ class RoomsBase { await factory.write(); })); - Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsBase.Keys.BuiltinGenerated), "1"); + Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Rooms.Root, RoomsMiscBase.Keys.BuiltinGenerated), "1"); return false; } - async getIdFromName(name: string) { - const unparsedId = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Room_Names, name)); - if (!unparsedId) return null; - const parsedId = parseInt(unparsedId); - if (isNaN(parsedId)) return null; - return parsedId; - } - - async getSubroomIdsFromRoom(id: number): Promise; - async getSubroomIdsFromRoom(id: number, stringify: false): Promise; - async getSubroomIdsFromRoom(id: number, stringify: boolean | undefined = false): Promise { - const ids = await Redis.Database.smembers(Redis.buildKey( - Redis.KeyGroups.Rooms.Root, - id.toString(), - RoomFactory.Keys.Subrooms - )); - const parsedIds = ids.map(val => parseInt(val)).filter(val => !isNaN(val)); - - if (!stringify) return parsedIds; - else return parsedIds.map(val => val.toString()); - } - - getSubroomNameFromId(room: RoomDetails, subroomId: number) { - const subroom = room.Scenes.find(scene => scene.RoomSceneId == subroomId); - if (subroom) return subroom.Name; - else return null; - } - - async socketUpdateRoom(instance: Instance) { - const room = await this.get(instance.roomId); - if (!room) return; - - for (const player of instance.getAllPlayers()) { - const sock = player.getSocketHandler(); - if (!sock) continue; - sock.sendNotification("RoomInstanceUpdate", instance.snapshot()); - sock.sendNotification(PushNotificationId.SubscriptionUpdateRoom, room); - } - } - } -const Rooms = new RoomsBase(); +const RoomsMisc = new RoomsMiscBase(); -export { rooms as BuiltinRooms }; -export default Rooms; \ No newline at end of file +export { builtinRooms as BuiltinRooms }; +export default RoomsMisc; \ No newline at end of file diff --git a/src/data/content/rooms/SubroomFactory.ts b/src/data/content/rooms/SubroomFactory.ts index 874f905..9fc7b33 100644 --- a/src/data/content/rooms/SubroomFactory.ts +++ b/src/data/content/rooms/SubroomFactory.ts @@ -15,15 +15,18 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ +import Logging from "@proxnet/undead-logging"; import { Redis } from "../../../db.ts"; -import { RoomDataTypes, IntegratedRoomScene, RoomScene, WriteMode, FactoryMode } from "./DataTypes.ts"; -import { RoomFactory } from "./RoomFactory.ts"; +import Server from "../../server/server.ts"; +import { RoomDataTypes } from "./base/DataTypes.ts"; +import { RoomFactory, roomdebug } from "./RoomFactory.ts"; + +const log = new Logging("SubroomFactory"); interface SubroomFactoryOptions { - roomId: number; subroomId: number; - writeMode?: WriteMode; - factoryMode?: FactoryMode; + writeMode?: RoomDataTypes.WriteMode; + factoryMode?: RoomDataTypes.FactoryMode; } export class SubroomFactory { @@ -42,13 +45,11 @@ export class SubroomFactory { #writeMode: RoomDataTypes.WriteMode = RoomDataTypes.WriteMode.WriteIfFree; factoryMode: RoomDataTypes.FactoryMode = RoomDataTypes.FactoryMode.Fetch; - #roomId: number; #subroomId: number; #hash: Record | null = null; constructor(options: SubroomFactoryOptions) { - this.#roomId = options.roomId; this.#subroomId = options.subroomId; if (options.writeMode) this.#writeMode = options.writeMode; if (options.factoryMode) this.factoryMode = options.factoryMode; @@ -56,12 +57,10 @@ export class SubroomFactory { async init() { - if (!this.#roomId || !this.#subroomId) throw this.#unspecifiedArguments; + if (!this.#subroomId) throw this.#unspecifiedArguments; this.#hash = await Redis.Database.hgetall(Redis.buildKey( - Redis.KeyGroups.Rooms.Root, - this.#roomId.toString(), - RoomFactory.Keys.Subrooms, + Redis.KeyGroups.Subrooms.Root, this.#subroomId.toString(), SubroomFactory.Keys.Meta )); @@ -76,9 +75,7 @@ export class SubroomFactory { else { const dbkey = Redis.buildKey( - Redis.KeyGroups.Rooms.Root, - this.#roomId.toString(), - RoomFactory.Keys.Subrooms, + Redis.KeyGroups.Subrooms.Root, this.#subroomId.toString(), SubroomFactory.Keys.Meta ); @@ -89,21 +86,25 @@ export class SubroomFactory { } if (!this.#hash) throw this.#hashValuesNotSetError; - this.#hash['DataModifiedAt'] = new Date().toISOString(); + this.#hash[this.#modifiedKey] = new Date().toISOString(); await Redis.Database.hset(dbkey, this.#hash); } - await Redis.Database.sadd(Redis.buildKey( - Redis.KeyGroups.Rooms.Root, - this.#roomId.toString(), - RoomFactory.Keys.Subrooms - ), this.RoomSceneId); + if (this.#hash[this.#roomIdKey]) + await Redis.Database.sadd(Redis.buildKey( + Redis.KeyGroups.Rooms.Root, + this.#hash[this.#roomIdKey].toString(), + RoomFactory.Keys.Subrooms + ), this.RoomSceneId); + if (roomdebug) log.d(`Writing subroom ${this.RoomSceneId}: ${JSON.stringify(this.#hash)}`); + Server.emit('room.subroom.updated', { subroom: this }); } - export(): RoomScene { + export(): RoomDataTypes.RoomScene { + if (roomdebug) log.d(`Exported subroom details for room:subroom ${this.RoomId}:${this.RoomSceneId}`); return { RoomSceneId: this.RoomSceneId, RoomId: this.RoomId, @@ -144,7 +145,7 @@ export class SubroomFactory { #setHashValue(key: string, value: string | number | boolean) { if (!this.#hash && this.factoryMode == RoomDataTypes.FactoryMode.Fetch) throw this.#mustFetchSubroomFirstError; - + if (!this.#hash) this.#hash = {}; if (typeof value === 'object' && value !== null) { @@ -158,10 +159,17 @@ export class SubroomFactory { get RoomSceneId() { return this.#subroomId } - get RoomId() { return this.#roomId } + #roomIdKey = 'RoomId'; + get RoomId() { return this.#fetchNumberKey(this.#roomIdKey, 0) } + set RoomId(data) { this.#setHashValue(this.#roomIdKey, data) } #locationKey = 'RoomSceneLocationId'; - get RoomSceneLocationId(): IntegratedRoomScene { return this.#fetchStringKey(this.#locationKey, IntegratedRoomScene.PerformanceHall) as IntegratedRoomScene } + get RoomSceneLocationId(): RoomDataTypes.IntegratedRoomScene { + return this.#fetchStringKey( + this.#locationKey, + RoomDataTypes.IntegratedRoomScene.PerformanceHall + ) as RoomDataTypes.IntegratedRoomScene; + } set RoomSceneLocationId(data) { this.#setHashValue(this.#locationKey, data) } #nameKey = 'Name'; @@ -189,9 +197,7 @@ export class SubroomFactory { async addBlobHistory(date: Date, filename: string) { await Redis.Database.hset(Redis.buildKey( - Redis.KeyGroups.Rooms.Root, - this.#roomId.toString(), - RoomFactory.Keys.Subrooms, + Redis.KeyGroups.Subrooms.Root, this.#subroomId.toString(), SubroomFactory.Keys.Blobs ), date.toISOString(), filename); @@ -204,9 +210,7 @@ export class SubroomFactory { } const hist = await Redis.Database.hgetall(Redis.buildKey( - Redis.KeyGroups.Rooms.Root, - this.#roomId.toString(), - RoomFactory.Keys.Subrooms, + Redis.KeyGroups.Subrooms.Root, this.#subroomId.toString(), SubroomFactory.Keys.Blobs )); diff --git a/src/data/content/rooms/DataTypes.ts b/src/data/content/rooms/base/DataTypes.ts similarity index 100% rename from src/data/content/rooms/DataTypes.ts rename to src/data/content/rooms/base/DataTypes.ts diff --git a/src/data/content/rooms/base/RoomsBase.ts b/src/data/content/rooms/base/RoomsBase.ts new file mode 100644 index 0000000..3816989 --- /dev/null +++ b/src/data/content/rooms/base/RoomsBase.ts @@ -0,0 +1,68 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import { Redis } from "../../../../db.ts"; +import { RoomDetails } from "./DataTypes.ts"; +import { RoomFactory } from "../RoomFactory.ts"; + +export class RoomsBase { + + async get(id: number) { + try { + const factory = await new RoomFactory({ id: id }).init(); + if (!factory) return null; + return factory.export(); + } catch { + return null; + } + } + + async getByName(name: string) { + try { + const factory = await new RoomFactory({ name: name }).init(); + if (!factory) return null; + return factory.export(); + } catch { + return null; + } + } + + async getSubroomIdsFromRoom(id: number): Promise; + async getSubroomIdsFromRoom(id: number, stringify: false): Promise; + async getSubroomIdsFromRoom(id: number, stringify: boolean | undefined = false): Promise { + const ids = await Redis.Database.smembers(Redis.buildKey( + Redis.KeyGroups.Rooms.Root, + id.toString(), + RoomFactory.Keys.Subrooms + )); + const parsedIds = ids.map(val => parseInt(val)).filter(val => !isNaN(val)); + + if (!stringify) return parsedIds; + else return parsedIds.map(val => val.toString()); + } + + getSubroomNameFromId(room: RoomDetails, subroomId: number) { + const subroom = room.Scenes.find(scene => scene.RoomSceneId == subroomId); + if (subroom) return subroom.Name; + else return null; + } + +} + +const Rooms = new RoomsBase(); + +export default Rooms; \ No newline at end of file diff --git a/src/data/live/Instance.ts b/src/data/live/Instance.ts new file mode 100644 index 0000000..c4fdb5e --- /dev/null +++ b/src/data/live/Instance.ts @@ -0,0 +1,188 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import { Config } from "../../config/config.ts"; +import { PushNotificationId } from "../../socket/types.ts"; +import { RoomDataTypes } from "../content/rooms/base/DataTypes.ts"; +import { RoomFactory } from "../content/rooms/RoomFactory.ts"; +import type { Profile } from "../profile/base/profiles.ts"; +import Server from "../server/server.ts"; +import Instances from "./instances.ts"; +import Presence from "./presence.ts"; +import type { InstanceOptions, RoomInstance } from "./types.ts"; +import Logging from "@proxnet/undead-logging"; + +const config = Config.getConfig(); + +const log = new Logging("InstanceBase"); + +export class Instance { + + #players = new Set(); + timeCreated = new Date().toISOString(); + + #id: number; + #room: RoomDataTypes.RoomDetails | undefined; + #subroom: RoomDataTypes.RoomScene | undefined; + + #eventId?: number; // not yet implemented + #name?: string; + #priv?: boolean; + #inProgress?: boolean; + #blob?: string; + + constructor(id: number) { + this.#id = id; + } + + async init(options: InstanceOptions) { + const scene = options.Room.Scenes[options.SceneIndex]; + if (!scene) throw new Error("The specified scene does not exist."); + + let instanceName; + if (scene.Name == 'Home' || scene.Name === options.Room.Room.Name) instanceName = `^${options.Room.Room.Name}`; + else instanceName = `^${options.Room.Room.Name}.${scene.Name}`; + if (options.IsDorm) { + const dormCreatorPlayer = Server.UnifiedProfile.get(options.Room.Room.CreatorPlayerId); + if (!dormCreatorPlayer) throw new Error("Creator of dorm does not exist."); + const player = await dormCreatorPlayer.export(); + if (player) instanceName = `@${player.displayName}'s Dorm`; + } + + this.#room = options.Room; + this.#subroom = scene; + this.#name = instanceName; + this.#blob = scene.DataBlobName; + this.#inProgress = false; + this.#priv = options.Private ? options.Private : false; + + return this; + + } + + equalInstance(instance: RoomInstance) { + return instance.roomInstanceId == this.#id; + } + + getAllPlayers() { + return this.#players.values().toArray(); + } + + hasPlayer(player: Profile) { + return this.getAllPlayers().includes(player); + } + + removePlayer(player: Profile) { + if (!this.hasPlayer(player)) throw new Error(`Cannot remove player ${player.getId()} from instance ${this.#id} they are not in`); + this.#players.delete(player); + player.setInstance(null); + } + + /** + * The client has a push notification for game session updates, but the client + * is based on instances, not game sessions. Possibly simply just an alias for + * 'RoomInstanceUpdate' but was never (or not yet) renamed. It is unknown + * whether the game uses this or not, but it's better to send it just in case. + */ + updatePlayers() { + for (const player of this.#players.values()) player.getSocketHandler()?.sendNotification(PushNotificationId.SubscriptionUpdateGameSession, this.snapshot()); + } + + async addPlayer(player: Profile) { + const currentInstance = player.getInstance(); + if (currentInstance && currentInstance.equalInstance(this)) return; + + if (currentInstance) currentInstance.removePlayer(player); + + if (!this.isFull) { + const instancePlayers = this.getAllPlayers(); + const profileExport = await player.export(); + log.i(`Player ${player.getId()} "${profileExport?.displayName}" went to '${this.name}' with ${instancePlayers.length} other players`); + + this.#players.add(player); + player.setInstance(this); + + const pres = await Presence.get(player); + pres.update(); + + const room = await new RoomFactory({ id: this.roomId }).init(); + await room?.addVisit(); + + // move some of this to a dedicated "onPlayerMove" function + } else log.w(`Instance ${this.roomInstanceId} is full. Cannot add player ${player.getId()}`); + + log.d(`Players in instance ${this.#id}: ${this.#players.values().toArray().map(prof => prof.getId()).join(',')}`); + } + + get roomInstanceId() { return this.#id } + + get roomId() { return this.#room ? this.#room?.Room.RoomId : 0 } + + get subRoomId() { return this.#subroom ? this.#subroom?.RoomSceneId : 0 } + + get location() { return this.#subroom ? this.#subroom?.RoomSceneLocationId : "" } + + get dataBlob() { return this.#blob ? this.#blob : undefined } + set dataBlob(data) { this.#blob = data } + + get eventId() { return this.#eventId } + + get photonRegionId() { return config.public.photonRegionId } + + get photonRoomId() { return `GC20200306-${this.#id}` } + + get name() { return this.#name ? this.#name : "InstanceNameError" } + + get maxCapacity() { return this.#subroom ? this.#subroom.MaxPlayers : 8 } + + get isFull() { return this.#players.size >= this.maxCapacity } + + get isPrivate() { return this.#priv ? this.#priv : false } + set isPrivate(data) { this.#priv = data } + + get isInProgress() { return this.#inProgress ? this.#inProgress : false } + set isInProgress(data) { this.#inProgress = data } + + snapshot() { + const inst: RoomInstance = { + roomInstanceId: this.roomInstanceId, + roomId: this.roomId, + subRoomId: this.subRoomId, + location: this.location, + dataBlob: this.dataBlob, + eventId: this.eventId, + photonRegionId: this.photonRegionId, + photonRoomId: this.photonRoomId, + name: this.name, + maxCapacity: this.maxCapacity, + isFull: this.isFull, + isPrivate: this.isPrivate, + isInProgress: this.isInProgress + } + return inst; + } + + destroy() { + Instances.getAllInstances(true).delete(this); + if (this.#players.size !== 0) for (const player of this.#players) player.getSocketHandler()?.sendNotification(PushNotificationId.Logout); + } + + updateSubroom(subroom: RoomDataTypes.RoomScene) { + this.#subroom = subroom; + } + +} \ No newline at end of file diff --git a/src/data/live/PhotonTypes.ts b/src/data/live/PhotonTypes.ts new file mode 100644 index 0000000..863c840 --- /dev/null +++ b/src/data/live/PhotonTypes.ts @@ -0,0 +1,48 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +export enum PhotonRegionCodeString { + Europe = "eu", + UnitedStates = "us", + Asia = "asia", + Japan = "jp", + Australia = "au", + UnitedStates_West = "usw", + SouthAmerica = "sa", + CanadaEast = "cae", + SouthKorea = "kr", + India = "@in", + Russia = "ru", + RussiaEast = "rue", + None = "none" +} + +export enum PhotonRegionCodeNumber { + eu, + us, + asia, + jp, + au = 5, + usw, + sa, + cae, + kr, + "@in", + ru, + rue, + none = 4 +} \ No newline at end of file diff --git a/src/data/live/base.ts b/src/data/live/base.ts index 21e5c09..9a07258 100644 --- a/src/data/live/base.ts +++ b/src/data/live/base.ts @@ -15,12 +15,16 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import Rooms from "../content/rooms.ts"; -import { RoomDataTypes } from "../content/rooms/DataTypes.ts"; -import { Profile } from "../profiles.ts"; +import Logging from "@proxnet/undead-logging"; +import RoomsMisc from "../content/rooms/Rooms.ts"; +import { RoomDataTypes } from "../content/rooms/base/DataTypes.ts"; +import Rooms from "../content/rooms/base/RoomsBase.ts"; +import { Profile } from "../profile/base/profiles.ts"; import Instances from "./instances.ts"; import { MatchmakingErrorCode, RoomInstance } from "./types.ts"; +const log = new Logging("MatchmakingBase"); + const loginLocks: Map = new Map(); interface MatchmakingOptions { @@ -49,6 +53,9 @@ class MatchmakingBase { else return null; } + /** + * @deprecated This will be removed as login locks will be saved to the database and cannot then be changed + */ deleteLoginLock(prof: Profile) { loginLocks.delete(prof.getId()); } @@ -75,7 +82,7 @@ class MatchmakingBase { } else { // check to make sure room exists, is not private, and is active - const targetRoom = options.roomName !== 'DormRoom' ? await Rooms.getByName(options.roomName) : await Rooms.getProfileDormDefault(options.profile); + const targetRoom = options.roomName !== 'DormRoom' ? await Rooms.getByName(options.roomName) : await RoomsMisc.getProfileDormDefault(options.profile); if (!targetRoom) return { errorCode: MatchmakingErrorCode.NoSuchRoom }; if (targetRoom.Room.Accessibility == RoomDataTypes.RoomAccessibility.Private && targetRoom.Room.CreatorPlayerId !== options.profile.getId()) return { errorCode: MatchmakingErrorCode.RoomIsPrivate }; @@ -90,7 +97,7 @@ class MatchmakingBase { if (subroomId) allInstances = allInstances.filter(instance => instance.subRoomId == subroomId); // filter instances that do not support join in progress and are in progress - const builtinRooms = Rooms.getAllBuiltinRooms(); + const builtinRooms = RoomsMisc.getAllBuiltinRooms(); const joinInProgressSubrooms = builtinRooms.map(room => ({Name: room.Name, Scenes: room.Scenes.map(scene => ({Name: scene.Name, Supported: scene.SupportsJoinInProgress}) @@ -117,9 +124,11 @@ class MatchmakingBase { if (!foundInstance) { const matchmakeableSubrooms = targetRoom.Scenes.filter(scene => scene.CanMatchmakeInto); + const index = Math.floor(Math.random() * matchmakeableSubrooms.length); + log.d(`Scene index ${index} (${targetRoom.Scenes[index] ? targetRoom.Scenes[index].RoomSceneId : "unknown"}) was chosen for matchmaking into ${targetRoom.Room.RoomId}`); const newInstance = await Instances.createInstance({ Room: targetRoom, - SceneIndex: Math.floor(Math.random() * matchmakeableSubrooms.length), + SceneIndex: index, FirstPlayer: options.profile, Private: options.private, IsDorm: options.roomName == 'DormRoom' diff --git a/src/data/live/instances.ts b/src/data/live/instances.ts index 88ad94f..0540ab8 100644 --- a/src/data/live/instances.ts +++ b/src/data/live/instances.ts @@ -16,171 +16,30 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import Logging from "@proxnet/undead-logging"; -import { Profile } from "../profiles.ts"; -import { RoomInstance, InstanceOptions } from "./types.ts"; -import { Config } from "../../config.ts"; -import Presence from "./presence.ts"; -import { RoomFactory } from "../content/rooms/RoomFactory.ts"; -import { RoomDataTypes } from "../content/rooms/DataTypes.ts"; -import { PushNotificationId } from "../../socket/types.ts"; -import Server from "../server.ts"; +import { InstanceOptions } from "./types.ts"; +import Server from "../server/server.ts"; +import { Instance } from "./Instance.ts"; const log = new Logging("Instances"); -const config = Config.getConfig(); - /** * `Map` */ const instanceSet: Set = new Set(); -export class Instance { +Server.on('room.subroom.updated', (ev) => { + if (instanceSet.values().map(inst => inst.subRoomId).toArray().includes(ev.subroom.RoomSceneId)) { + const instance = instanceSet.values().toArray().find(inst => inst.subRoomId == ev.subroom.RoomSceneId); + if (!instance) return; - #players = new Set(); - timeCreated = new Date().toISOString(); - - #id: number; - #room: RoomDataTypes.RoomDetails | undefined; - #subroom: RoomDataTypes.RoomScene | undefined; - - #eventId?: number; // not yet implemented - #name?: string; - #priv?: boolean; - #inProgress?: boolean; - #blob?: string; - - constructor(id: number) { - this.#id = id; + instance.dataBlob = ev.subroom.DataBlobName; + instance.updateSubroom(ev.subroom.export()); + for (const profile of instance.getAllPlayers()) + profile.getSocketHandler()?.sendNotification('RoomInstanceUpdate', instance.snapshot()); + + instance.updatePlayers(); // legacy } - - async init(options: InstanceOptions) { - - const scene = options.Room.Scenes[options.SceneIndex]; - if (!scene) throw new Error("The specified scene does not exist."); - - let instanceName; - if (scene.Name == 'Home' || scene.Name === options.Room.Room.Name) instanceName = `^${options.Room.Room.Name}`; - else instanceName = `^${options.Room.Room.Name}.${scene.Name}`; - if (options.IsDorm) { - const dormCreatorPlayer = Server.UnifiedProfile.get(options.Room.Room.CreatorPlayerId); - if (!dormCreatorPlayer) throw new Error("Creator of dorm does not exist."); - const player = await dormCreatorPlayer.export(); - if (player) instanceName = `@${player.displayName}'s Dorm`; - } - - this.#room = options.Room; - this.#subroom = scene; - this.#name = instanceName; - this.#blob = scene.DataBlobName; - this.#inProgress = false; - this.#priv = options.Private ? options.Private : false; - - return this; - - } - - equalInstance(instance: RoomInstance) { - return instance.roomInstanceId == this.#id; - } - - getAllPlayers() { - return this.#players.values().toArray(); - } - - hasPlayer(player: Profile) { - return this.getAllPlayers().includes(player); - } - - removePlayer(player: Profile) { - if (!this.hasPlayer(player)) throw new Error(`Cannot remove player ${player.getId()} from instance ${this.#id} they are not in`); - this.#players.delete(player); - player.setInstance(null); - } - - updatePlayers() { - for (const player of this.#players.values()) player.getSocketHandler()?.sendNotification(PushNotificationId.SubscriptionUpdateGameSession, this.snapshot()); - } - - async addPlayer(player: Profile) { - const currentInstance = player.getInstance(); - if (currentInstance && currentInstance.equalInstance(this)) return; - - if (currentInstance) currentInstance.removePlayer(player); - - if (!this.isFull) { - const instancePlayers = this.getAllPlayers(); - const profileExport = await player.export(); - log.i(`Player ${player.getId()} "${profileExport?.displayName}" went to '${this.name}' with ${instancePlayers.length} other players`); - - this.#players.add(player); - player.setInstance(this); - - const pres = await Presence.get(player); - pres.update(); - - const room = await new RoomFactory({ id: this.roomId }).init(); - await room?.addVisit(); - - // move some of this to a dedicated "onPlayerMove" function - } else log.w(`Instance ${this.roomInstanceId} is full. Cannot add player ${player.getId()}`); - - log.d(`Players in instance ${this.#id}: ${this.#players.values().toArray().map(prof => prof.getId()).join(',')}`); - } - - get roomInstanceId() { return this.#id } - - get roomId() { return this.#room ? this.#room?.Room.RoomId : 0 } - - get subRoomId() { return this.#subroom ? this.#subroom?.RoomSceneId : 0 } - - get location() { return this.#subroom ? this.#subroom?.RoomSceneLocationId : "" } - - get dataBlob() { return this.#blob ? this.#blob : undefined } - set dataBlob(data) { this.#blob = data } - - get eventId() { return this.#eventId } - - get photonRegionId() { return config.public.photonRegionId } - - get photonRoomId() { return `GC20200306-${this.#id}` } - - get name() { return this.#name ? this.#name : "InstanceNameError" } - - get maxCapacity() { return this.#subroom ? this.#subroom.MaxPlayers : 8 } - - get isFull() { return this.#players.size >= this.maxCapacity } - - get isPrivate() { return this.#priv ? this.#priv : false } - set isPrivate(data) { this.#priv = data } - - get isInProgress() { return this.#inProgress ? this.#inProgress : false } - set isInProgress(data) { this.#inProgress = data } - - snapshot() { - const inst: RoomInstance = { - roomInstanceId: this.roomInstanceId, - roomId: this.roomId, - subRoomId: this.subRoomId, - location: this.location, - dataBlob: this.dataBlob, - eventId: this.eventId, - photonRegionId: this.photonRegionId, - photonRoomId: this.photonRoomId, - name: this.name, - maxCapacity: this.maxCapacity, - isFull: this.isFull, - isPrivate: this.isPrivate, - isInProgress: this.isInProgress - } - return inst; - } - - destroy() { - instanceSet.delete(this); - if (this.#players.size !== 0) for (const player of this.#players) player.getSocketHandler()?.sendNotification(PushNotificationId.Logout); - } - -} +}); class InstancesBase { @@ -191,8 +50,9 @@ class InstancesBase { else return null; } - getAllInstances() { - return new Set([...instanceSet.values().toArray()]); + getAllInstances(asSet: boolean | undefined = false) { + if (!asSet) return new Set([...instanceSet.values().toArray()]); + else return instanceSet; } getAllRoomInstances(roomId: number) { diff --git a/src/data/live/presence.ts b/src/data/live/presence.ts index f68607a..e3ee02b 100644 --- a/src/data/live/presence.ts +++ b/src/data/live/presence.ts @@ -17,10 +17,13 @@ along with this program. If not, see . */ import { z } from "zod"; import { SettingKey } from "../content/settings.ts"; -import { Profile } from "../profiles.ts"; +import type { Profile } from "../profile/base/profiles.ts"; import { DeviceClass, PlayerStatusVisibility, RoomInstance, VRMovementMode } from "./types.ts"; import Logging from "@proxnet/undead-logging"; -import { Instance } from "./instances.ts"; +import { Instance } from "./Instance.ts"; +import { RoomUpdatedEvent } from "../content/rooms/RoomEvents.ts"; +import Server from "../server/server.ts"; +import { PushNotificationId } from "../../socket/types.ts"; const log = new Logging("Presence"); @@ -57,6 +60,7 @@ class PlayerPresence { this.updateOffline(); }, 80000); + Server.on('room.updated', this.#roomUpdatedEventCallback); } offline: boolean; @@ -69,6 +73,20 @@ class PlayerPresence { lastSeen: Date; + #roomUpdatedEventCallback = (ev: RoomUpdatedEvent) => { + //log.d(`Room ${ev.room.RoomId} updated, notifying client.`); + ev.room.export().then(roomDetails => { + log.d(`${this.roomInstance?.roomId} == ${ev.room.RoomId}, P:${this.playerId}`); + if (this.roomInstance?.roomId == ev.room.RoomId) { + const socket = this.getProfile().getSocketHandler(); + if (!socket) return; + + socket.sendNotification(PushNotificationId.SubscriptionUpdateRoom, roomDetails); + socket.presenceUpdate(); + } + }); + } + async updateStatusVisibility() { const PlayerStatusVisibilityEnum = z.nativeEnum(PlayerStatusVisibility); @@ -122,6 +140,16 @@ class PlayerPresence { this.lastSeen = new Date(); } + getProfile() { + return this.#profile; + } + + destroy() { + presence.delete(this); + clearInterval(this.intervalId); + Server.off('room.updated', this.#roomUpdatedEventCallback); + } + } const presence: Set = new Set(); @@ -164,10 +192,8 @@ class PresenceBase { deleteDeadPresences() { for (const pres of presence.values()) - if (Math.round(new Date().getTime() / 1000) - Math.round(pres.lastSeen.getTime() / 1000) >= 60) { - presence.delete(pres); - clearInterval(pres.intervalId); - } + if (Math.round(new Date().getTime() / 1000) - Math.round(pres.lastSeen.getTime() / 1000) >= 60) + pres.destroy(); } } diff --git a/src/data/live/types.ts b/src/data/live/types.ts index 3ab657a..e3cff17 100644 --- a/src/data/live/types.ts +++ b/src/data/live/types.ts @@ -15,40 +15,9 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { RoomDataTypes } from "../content/rooms/DataTypes.ts"; -import { Profile } from "../profiles.ts"; - -export enum PhotonRegionCodeString { - Europe = "eu", - UnitedStates = "us", - Asia = "asia", - Japan = "jp", - Australia = "au", - UnitedStates_West = "usw", - SouthAmerica = "sa", - CanadaEast = "cae", - SouthKorea = "kr", - India = "@in", - Russia = "ru", - RussiaEast = "rue", - None = "none" -} - -export enum PhotonRegionCodeNumber { - eu, - us, - asia, - jp, - au = 5, - usw, - sa, - cae, - kr, - "@in", - ru, - rue, - none = 4 -} +import { RoomDataTypes } from "../content/rooms/base/DataTypes.ts"; +import type { Profile } from "../profile/base/profiles.ts"; +import { PhotonRegionCodeString, PhotonRegionCodeNumber } from "./PhotonTypes.ts"; export interface RoomInstance { diff --git a/src/data/profile/ProfileAuth.ts b/src/data/profile/ProfileAuth.ts new file mode 100644 index 0000000..5b96710 --- /dev/null +++ b/src/data/profile/ProfileAuth.ts @@ -0,0 +1,44 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import { Config } from "../../config/config.ts"; +import { Redis } from "../../db.ts"; +import type { ProfileTokenFormat } from "../auth/TokenBaseFormat.ts"; +import { AuthType } from "../UserTypes.ts"; +import { ProfileContentManager } from "./base/profilemanagerbase.ts"; +import * as JsonWebToken from "@gz/jwt"; + +const config = Config.getConfig(); + +export class ProfileAuth extends ProfileContentManager { + + async getToken() { + const payload: ProfileTokenFormat = { + iss: `${config.web.api.securepublichost ? 'https' : 'http'}://${config.web.api.publichost}`, + sub: this.profile.getId(), + role: (await this.getIsOperator()) ? 'developer' : 'user', + exp: Math.round(Date.now() / 1000) + Math.round(config.auth.timeout * 60 * 60), + typ: AuthType.Game, + }; + return await JsonWebToken.encode(payload, config.auth.secret, { algorithm: "HS512" }); + } + + async getIsOperator() { + return (await Redis.Database.sismember(Redis.buildKey(Redis.KeyGroups.Operators), this.profile.getId().toString())) >= 1; + } + +} \ No newline at end of file diff --git a/src/data/profile/avatar.ts b/src/data/profile/avatar.ts index 199ddf1..7a85abf 100644 --- a/src/data/profile/avatar.ts +++ b/src/data/profile/avatar.ts @@ -29,7 +29,7 @@ export class ProfileAvatarManager extends ProfileContentManager { #rootKey = Redis.buildKey( Redis.KeyGroups.Profiles.Root, - this.profileId.toString(), + this.profile.getId().toString(), Redis.KeyGroups.Profiles.Avatar.Root ); diff --git a/src/data/profile/base/profilemanagerbase.ts b/src/data/profile/base/profilemanagerbase.ts index 7e27d79..487124d 100644 --- a/src/data/profile/base/profilemanagerbase.ts +++ b/src/data/profile/base/profilemanagerbase.ts @@ -15,16 +15,18 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ +import type { Profile } from "./profiles.ts"; + export class ProfileContentManager { - constructor(profileId: number) { - this.profileId = profileId; + constructor(profile: Profile) { + this.profile = profile; } onProfileInit() { return; } - profileId: number; + profile: Profile; } \ No newline at end of file diff --git a/src/data/profiles.ts b/src/data/profile/base/profiles.ts similarity index 82% rename from src/data/profiles.ts rename to src/data/profile/base/profiles.ts index b2eed5a..fa5eb51 100644 --- a/src/data/profiles.ts +++ b/src/data/profile/base/profiles.ts @@ -15,28 +15,23 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { Redis } from "../db.ts"; +import { Redis } from "../../../db.ts"; import Logging from "@proxnet/undead-logging"; -import Dictionary from "./usernames.ts"; -import { Config } from "../config.ts"; -import { AuthType } from "./users.ts"; -import * as JsonWebToken from "@gz/jwt"; -import { TokenBaseFormat } from "../apiutils.ts"; -import { DeviceClass, VRMovementMode } from "./live/types.ts"; -import { SettingKey } from "./content/settings.ts"; +import Dictionary from "../../usernames.ts"; +import { DeviceClass, VRMovementMode } from "../../live/types.ts"; +import { SettingKey } from "../../content/settings.ts"; import { z } from "zod"; -import { SignalRSocketHandler } from "../socket/socket.ts"; -import { ProfileSettingsManager } from "./profile/settings.ts"; -import { ProfileProgressionManager } from "./profile/progression.ts"; -import { ProfileReputationManager } from "./profile/reputation.ts"; -import { ProfileRelationshipManager } from "./profile/relationships.ts"; -import { ProfileAvatarManager } from "./profile/avatar.ts"; -import { EventManager } from "./baseevent.ts"; -import { ProfileEvents, ProfileUpdatedEvent } from "./profileevents.ts"; -import { Instance } from "./live/instances.ts"; -import { ProfileRoomsManager } from "./profile/rooms.ts"; - -const config = Config.getConfig(); +import { SignalRSocketHandler } from "../../../socket/socket.ts"; +import { ProfileSettingsManager } from "../settings.ts"; +import { ProfileProgressionManager } from "../progression.ts"; +import { ProfileReputationManager } from "../reputation.ts"; +import { ProfileRelationshipManager } from "../relationships.ts"; +import { ProfileAvatarManager } from "../avatar.ts"; +import type { ProfileUpdatedEvent } from "../../profileevents.ts"; +import { Instance } from "../../live/Instance.ts"; +import { ProfileRoomsManager } from "../rooms.ts"; +import { type ServerBase } from "../../server/server.ts"; +import { ProfileAuth as ProfileAuthManager } from "../ProfileAuth.ts"; const log = new Logging("Profiles"); @@ -63,15 +58,10 @@ export interface SelfAccountExport extends AccountExport { juniorState?: number, parentAccountId?: number } -export interface ProfileTokenFormat extends TokenBaseFormat { - sub: number; - role: "developer" | "user"; - typ: AuthType.Game; -} const reservedIds = [1, 2]; -class Profile extends EventManager { +class Profile { static async exists(id: number) { return (await Redis.Database.exists( Redis.buildKey( @@ -202,26 +192,25 @@ class Profile extends EventManager { Relationships: ProfileRelationshipManager; Avatar: ProfileAvatarManager; Rooms: ProfileRoomsManager; + Auth: ProfileAuthManager; constructor(id: number) { - super(); - this.#id = id; - this.Settings = new ProfileSettingsManager(this.#id); - this.Progression = new ProfileProgressionManager(this.#id); - this.Reputation = new ProfileReputationManager(this.#id); - this.Relationships = new ProfileRelationshipManager(this.#id); - this.Avatar = new ProfileAvatarManager(this.#id); - this.Rooms = new ProfileRoomsManager(this.#id); + this.Settings = new ProfileSettingsManager(this); + this.Progression = new ProfileProgressionManager(this); + this.Reputation = new ProfileReputationManager(this); + this.Relationships = new ProfileRelationshipManager(this); + this.Avatar = new ProfileAvatarManager(this); + this.Rooms = new ProfileRoomsManager(this); + this.Auth = new ProfileAuthManager(this); } #emitProfileUpdated() { const ev: ProfileUpdatedEvent = { - time: new Date(), profile: this } - this.emit(ProfileEvents.BaseUpdated, ev); + if (Server) Server.emit('profile.updated', ev); } setInstance(instance: Instance | null) { @@ -236,10 +225,6 @@ class Profile extends EventManager { return this.#id; } - async getIsOperator() { - return (await Redis.Database.sismember(Redis.buildKey(Redis.KeyGroups.Operators), this.#id.toString())) >= 1; - } - async getBio() { const bio = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Bio)); if (!bio) return ""; @@ -330,26 +315,20 @@ class Profile extends EventManager { getSocketHandler() { return this.#socket; } - - // get, set instance - // this.#instance: RoomInstance - - async getToken() { - const payload: ProfileTokenFormat = { - iss: `${config.web.api.securepublichost ? 'https' : 'http'}://${config.web.api.publichost}`, - sub: this.#id, - role: (await this.getIsOperator()) ? 'developer' : 'user', - exp: Math.round(Date.now() / 1000) + Math.round(config.auth.timeout * 60 * 60), - typ: AuthType.Game, - }; - return await JsonWebToken.encode(payload, config.auth.secret, { algorithm: "HS512" }); - } + } -const profiles: Map = new Map() +const profiles: Map = new Map(); + +// surely this can be fixed +let Server: ServerBase | undefined; export class UnifiedProfileBase { + constructor(server: ServerBase) { + Server = server; + } + get(id: number) { let profile = profiles.get(id); if (!profile) { @@ -357,7 +336,8 @@ export class UnifiedProfileBase { const inst = new Profile(id); profiles.set(id, inst); profile = inst; - } catch { + } catch (err) { + log.e(`Could not fetch profile: ${(err as Error).stack}`); return null; } } @@ -365,7 +345,10 @@ export class UnifiedProfileBase { } async create(options: ProfileInitOptions) { - return await Profile.init(options); + const profile = await Profile.init(options); + if (!profile) return null; + profiles.set(profile.getId(), profile); + return profile; } async exists(id: number) { diff --git a/src/data/profile/progression.ts b/src/data/profile/progression.ts index 33ebe26..0605957 100644 --- a/src/data/profile/progression.ts +++ b/src/data/profile/progression.ts @@ -16,13 +16,13 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import Logging from "@proxnet/undead-logging"; -import { GameConfigs } from "../config.ts"; import { ProfileContentManager } from "./base/profilemanagerbase.ts"; import { Redis } from "../../db.ts"; +import { getPublicConfig } from "../config/PublicConfig.ts"; const log = new Logging("ProfileProgression"); -const config = GameConfigs.getConfig(); +const config = getPublicConfig(); interface PlayerProgressionExport { PlayerId: number, @@ -34,7 +34,7 @@ export class ProfileProgressionManager extends ProfileContentManager { async export() { const ex: PlayerProgressionExport = { - PlayerId: this.profileId, + PlayerId: this.profile.getId(), Level: await this.getLevel(), XP: await this.getXp() } @@ -47,7 +47,7 @@ export class ProfileProgressionManager extends ProfileContentManager { * @returns The new # of XP */ async setXp(xp: number) { - await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Xp), xp.toString()); + await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Xp), xp.toString()); return xp; } @@ -85,12 +85,12 @@ export class ProfileProgressionManager extends ProfileContentManager { } async getXp() { - let data = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Xp)); + let data = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Xp)); if (data == null) data = (await this.setXp(0)).toString(); const parsedData = parseInt(data); if (isNaN(parsedData)) { - log.w(`Parsed xp data for ${this.profileId} is NaN!`); + log.w(`Parsed xp data for ${this.profile.getId()} is NaN!`); const one = config?.LevelProgressionMaps[1]; if (typeof one == 'undefined' && !one) return 0; // fallback since progression data is required else return one.RequiredXp; diff --git a/src/data/profile/relationships.ts b/src/data/profile/relationships.ts index 7eb62d7..9d50d60 100644 --- a/src/data/profile/relationships.ts +++ b/src/data/profile/relationships.ts @@ -16,9 +16,9 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { Redis } from "../../db.ts"; -import { Profile } from "../profiles.ts"; +import type { Profile } from "./base/profiles.ts"; import { ProfileContentManager } from "./base/profilemanagerbase.ts"; -import Server from "../server.ts"; +//import Server from "../server.ts"; // see circular dep comment at bottom enum RelationshipType { None, @@ -64,7 +64,7 @@ export class ProfileRelationshipManager extends ProfileContentManager { #rootKey = Redis.buildKey( Redis.KeyGroups.Profiles.Root, - this.profileId.toString(), + this.profile.getId().toString(), Redis.KeyGroups.Profiles.Relationships.Root ); @@ -144,8 +144,8 @@ export class ProfileRelationshipManager extends ProfileContentManager { async #clearAssociationWithRemote(remoteProfileId: number) { const remoteRootKey = this.#createRemoteRootKey(remoteProfileId); await Redis.Database.srem(this.#incomingFriends, remoteProfileId); - await Redis.Database.srem(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.OutgoingFriendRequests), this.profileId); - await Redis.Database.srem(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profileId); + await Redis.Database.srem(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.OutgoingFriendRequests), this.profile.getId()); + await Redis.Database.srem(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profile.getId()); await Redis.Database.srem(this.#outgoingFriends, remoteProfileId); } @@ -158,7 +158,7 @@ export class ProfileRelationshipManager extends ProfileContentManager { const remoteRootKey = this.#createRemoteRootKey(remoteProfileId); await Redis.Database.sadd(this.#friendsKey, remoteProfileId); - await Redis.Database.sadd(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.Friends), this.profileId); + await Redis.Database.sadd(Redis.buildKey(remoteRootKey, Redis.KeyGroups.Profiles.Relationships.Friends), this.profile.getId()); } async denyRequest(remoteProfileId: number) { @@ -182,7 +182,7 @@ export class ProfileRelationshipManager extends ProfileContentManager { const remoteKey = this.#createRemoteRootKey(remoteProfileId); const localMuted = (await this.getAllMuted()).includes(remoteProfileId); - const remoteMuted = await Redis.Database.sismember(Redis.buildKey(remoteKey, Redis.KeyGroups.Profiles.Relationships.Muted), this.profileId); + const remoteMuted = await Redis.Database.sismember(Redis.buildKey(remoteKey, Redis.KeyGroups.Profiles.Relationships.Muted), this.profile.getId()); if (localMuted && remoteMuted) return ReciprocalStatus.Mutual; else if (localMuted) return ReciprocalStatus.Local; @@ -194,7 +194,7 @@ export class ProfileRelationshipManager extends ProfileContentManager { const remoteKey = this.#createRemoteRootKey(remoteProfileId); const localIgnored = (await this.getAllMuted()).includes(remoteProfileId); - const remoteIgnored = await Redis.Database.sismember(Redis.buildKey(remoteKey, Redis.KeyGroups.Profiles.Relationships.Ignoring), this.profileId); + const remoteIgnored = await Redis.Database.sismember(Redis.buildKey(remoteKey, Redis.KeyGroups.Profiles.Relationships.Ignoring), this.profile.getId()); if (localIgnored && remoteIgnored) return ReciprocalStatus.Mutual; else if (localIgnored) return ReciprocalStatus.Local; @@ -213,22 +213,23 @@ export class ProfileRelationshipManager extends ProfileContentManager { } async sendPlayerFriendRequest(player: Profile) { - await Redis.Database.sadd(Redis.buildKey(this.#createRemoteRootKey(player.getId()), Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profileId); + await Redis.Database.sadd(Redis.buildKey(this.#createRemoteRootKey(player.getId()), Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profile.getId()); await Redis.Database.sadd(this.#outgoingFriends, player.getId()); } async removePlayerFriendRequest(player: Profile) { - await Redis.Database.srem(Redis.buildKey(this.#createRemoteRootKey(player.getId()), Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profileId); + await Redis.Database.srem(Redis.buildKey(this.#createRemoteRootKey(player.getId()), Redis.KeyGroups.Profiles.Relationships.IncomingFriendRequests), this.profile.getId()); await Redis.Database.srem(this.#outgoingFriends, player.getId()); } - async ignoreAllAssociatedPlatformUsers(platformid: string) { - const ids = (await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, platformid))).map(val => parseInt(val)).filter(val => !isNaN(val)); + async ignoreAllAssociatedPlatformUsers(_platformid: string) { + /*const ids = (await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, platformid))).map(val => parseInt(val)).filter(val => !isNaN(val)); for (const id of ids) { const profile = Server.UnifiedProfile.get(id); if (!profile) continue; this.setPlayerIgnored(profile); - } + }*/ + // circular dep here. this will do nothing for now. } } \ No newline at end of file diff --git a/src/data/profile/reputation.ts b/src/data/profile/reputation.ts index a04d74e..6df12b8 100644 --- a/src/data/profile/reputation.ts +++ b/src/data/profile/reputation.ts @@ -22,7 +22,7 @@ export class ProfileReputationManager extends ProfileContentManager { // deno-lint-ignore require-await async getReputation() { // async temporary return { - AccountId: this.profileId, + AccountId: this.profile.getId(), Noteriety: 0.0, CheerGeneral: 0, CheerHelpful: 0, diff --git a/src/data/profile/rooms.ts b/src/data/profile/rooms.ts index e1dfbe1..c824e07 100644 --- a/src/data/profile/rooms.ts +++ b/src/data/profile/rooms.ts @@ -22,7 +22,7 @@ export class ProfileRoomsManager extends ProfileContentManager { #rootKey = Redis.buildKey( Redis.KeyGroups.Profiles.Root, - this.profileId.toString(), + this.profile.getId().toString(), Redis.KeyGroups.Profiles.Rooms ); diff --git a/src/data/profile/settings.ts b/src/data/profile/settings.ts index 767f2b6..c6b1669 100644 --- a/src/data/profile/settings.ts +++ b/src/data/profile/settings.ts @@ -31,30 +31,30 @@ export class ProfileSettingsManager extends ProfileContentManager { } async getSettings() { - const settings = await Redis.Database.hgetall(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings)); + const settings = await Redis.Database.hgetall(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Settings)); const returnSettings: Setting[] = []; for (const key of Object.keys(settings)) returnSettings.push({ Key: key, Value: settings[key] }); return returnSettings; } async getSetting(key: SettingKey) { - return await Redis.Database.hget(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings), key); + return await Redis.Database.hget(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Settings), key); } async setSetting(key: SettingKey, value: string | number) { - await Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings), key, value); + await Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Settings), key, value); } async setSettingRaw(key: string, value: string | number) { - await Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings), key, value); + await Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Settings), key, value); } async delSetting(key: SettingKey) { - await Redis.Database.hdel(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings), key); + await Redis.Database.hdel(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Settings), key); } async delAllSettings() { - await Redis.Database.del(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings)); + await Redis.Database.del(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profile.getId().toString(), Redis.KeyGroups.Profiles.Settings)); } } \ No newline at end of file diff --git a/src/data/profileevents.ts b/src/data/profileevents.ts index 26fec25..90661b7 100644 --- a/src/data/profileevents.ts +++ b/src/data/profileevents.ts @@ -15,13 +15,8 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { Event } from "./baseevent.ts"; -import { Profile } from "./profiles.ts"; +import { Profile } from "./profile/base/profiles.ts"; -export enum ProfileEvents { - BaseUpdated = "profile.updated" -} - -export interface ProfileUpdatedEvent extends Event { +export interface ProfileUpdatedEvent { profile: Profile } \ No newline at end of file diff --git a/src/data/server/server.ts b/src/data/server/server.ts new file mode 100644 index 0000000..7b50cfc --- /dev/null +++ b/src/data/server/server.ts @@ -0,0 +1,29 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import { EventManager } from "../baseevent.ts"; +import { CDNBase } from "../content/cdn.ts"; +import { UnifiedProfileBase } from "../profile/base/profiles.ts"; +import { ServerEvents } from "../server/serverevents.ts"; + +export class ServerBase extends EventManager { + CDN = new CDNBase(); + UnifiedProfile = new UnifiedProfileBase(this); +} + +const Server = new ServerBase(); +export default Server; \ No newline at end of file diff --git a/src/data/server/serverevents.ts b/src/data/server/serverevents.ts new file mode 100644 index 0000000..d47bb3b --- /dev/null +++ b/src/data/server/serverevents.ts @@ -0,0 +1,25 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import type { RoomUpdatedEvent, SubroomUpdatedEvent } from "../content/rooms/RoomEvents.ts"; +import { ProfileUpdatedEvent } from "../profileevents.ts"; + +export interface ServerEvents { + 'profile.updated': ProfileUpdatedEvent + 'room.subroom.updated': SubroomUpdatedEvent + 'room.updated': RoomUpdatedEvent +} \ No newline at end of file diff --git a/src/data/steam.ts b/src/data/steam.ts deleted file mode 100644 index 64d92e0..0000000 --- a/src/data/steam.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* Galvanic Corrosion - Rec Room custom server for communities. - -Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . */ - -import Logging from "@proxnet/undead-logging"; -import { Config } from "../config.ts"; - -const log = new Logging("Steam"); - -const config = Config.getConfig(); - -interface AuthenticateUserTicketSuccess { - result: 'OK', - steamid: string, - ownersteamid: string, - vacbanned: boolean, - publisherbanned: boolean -} -interface AuthenticateUserTicketError { - errorcode: number, - errordesc: string -} - -interface SteamRes { - response: { - error?: AuthenticateUserTicketError, - params?: AuthenticateUserTicketSuccess - } -} - -export async function AuthenticateUserTicket(ticket: string, userid: string) { - if (!config.auth.steamkey) return true; // always authenticate if no steam API key was found - - const params = new URLSearchParams(); - params.append('key', config.auth.steamkey); - params.append('appid', "471710"); - params.append('ticket', ticket); - - try { - const res = await fetch(`https://api.steampowered.com/ISteamUserAuth/AuthenticateUserTicket/v1?${params}`); - const resjson = (await res.json()) as SteamRes; - - if (resjson.response.error) { - log.w(`Steam Authentication failed: (${resjson.response.error.errorcode}) ${resjson.response.error.errordesc}`); - - // add more error codes later if needed - const conditions = [ - resjson.response.error.errorcode == 100 - ].includes(true); - if (conditions) log.w('This error indicates a client problem.'); - return false; - } - - log.d(JSON.stringify(resjson.response)); - if (resjson.response.params) return resjson.response.params.steamid === userid && resjson.response.params.ownersteamid === userid; - else { - log.w("Steam Authentication failed: Steam response did not contain params or error! This should never be logged!"); - return false; - } - } catch (err) { - log.w(`Steam Authentication failed: ${(err as Error).message}`); - return false; - } -} - -export * as Steam from "./steam.ts"; \ No newline at end of file diff --git a/src/data/steam/SteamAuthTypes.ts b/src/data/steam/SteamAuthTypes.ts new file mode 100644 index 0000000..f77243c --- /dev/null +++ b/src/data/steam/SteamAuthTypes.ts @@ -0,0 +1,55 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +interface AuthenticateUserTicketSuccess { + result: 'OK'; + steamid: string; + ownersteamid: string; + vacbanned: boolean; + publisherbanned: boolean; +} +interface AuthenticateUserTicketError { + errorcode: number; + errordesc: string; +} +export interface SteamAuthRes { + response: { + error?: AuthenticateUserTicketError; + params?: AuthenticateUserTicketSuccess; + }; +} + +export enum SteamAuthResult { + Success, + Failure, + NotConfigured +} +interface SteamAuthBase { + valid: SteamAuthResult; +} +interface SteamAuthSuccess extends SteamAuthBase { + valid: SteamAuthResult.Success; + res: AuthenticateUserTicketSuccess; +} +interface SteamAuthFailure extends SteamAuthBase { + valid: SteamAuthResult.Failure; + res: AuthenticateUserTicketError; +} +interface SteamAuthNotConfigured extends SteamAuthBase { + valid: SteamAuthResult.NotConfigured; +} +export type SteamAuth = SteamAuthSuccess | SteamAuthFailure | SteamAuthNotConfigured; \ No newline at end of file diff --git a/src/data/steam/SteamCommonTypes.ts b/src/data/steam/SteamCommonTypes.ts new file mode 100644 index 0000000..d33bb3f --- /dev/null +++ b/src/data/steam/SteamCommonTypes.ts @@ -0,0 +1,34 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import type { PersonaState, CommunityVisibilityState } from "./steam.ts"; + +export interface SteamPlayer { + steamid: string; + personaname: string; + profileurl: string; + avatar: string; + avatarmedium: string; + avatarfull: string; + personastate: PersonaState; + communityvisibilitystate: CommunityVisibilityState; + profilestate?: 1; + lastlogoff?: number; + commentpermission?: 1; + loccountrycode?: string; // undocumented but is seen in API responses - may or may not be undefined + locstatecode?: string; // undocumented but is seen in API responses - may or may not be undefined +} \ No newline at end of file diff --git a/src/data/steam/steam.ts b/src/data/steam/steam.ts new file mode 100644 index 0000000..53bb773 --- /dev/null +++ b/src/data/steam/steam.ts @@ -0,0 +1,111 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import Logging from "@proxnet/undead-logging"; +import { Config } from "../../config/config.ts"; +import { SteamAuth, SteamAuthResult, SteamAuthRes } from "./SteamAuthTypes.ts"; +import { SteamPlayer } from "./SteamCommonTypes.ts"; + +const log = new Logging("Steam"); + +const config = Config.getConfig(); + +function buildSteamUrl(steaminterface: string, endpoint: string) { + return `https://api.steampowered.com/${steaminterface}/${endpoint}`; +} + +export enum PersonaState { + Offline, + Online, + Busy, + Away, + Snooze, + LookingToTrade, + LookingToPlay +} + +export enum CommunityVisibilityState { + NotVisible, + PubliclyVisible = 3 +} + +class SteamBase { + + async GetPlayerSummaries(steamids: string[]) { + if (!config.auth.steamkey) return null; + + const params = new URLSearchParams(); + params.append('key', config.auth.steamkey); + params.append('steamids', steamids.join(',')) + + try { + const res = await fetch(`${buildSteamUrl('ISteamUser', 'GetPlayerSummaries/v2')}?${params}`); + if (res.status !== 200) return null; + + const resjson = await res.json() as { response: { players: SteamPlayer[] } }; + return resjson.response.players; + } catch (err) { + log.e(`Could not fetch Steam player summaries: ${(err as Error).stack}`); + return null; + } + } + + async AuthenticateUserTicket(ticket: string, userid: string): Promise { + if (!config.auth.steamkey) return { valid: SteamAuthResult.NotConfigured }; // always authenticate if no steam API key was found + + const params = new URLSearchParams(); + params.append('key', config.auth.steamkey); + params.append('appid', "471710"); + params.append('ticket', ticket); + + try { + const res = await fetch(`${buildSteamUrl('ISteamUserAuth', 'AuthenticateUserTicket/v1')}?${params}`); + const resjson = (await res.json()) as SteamAuthRes; + + if (resjson.response.error) { + log.w(`Steam Authentication failed: (${resjson.response.error.errorcode}) ${resjson.response.error.errordesc}`); + + // add more error codes later if needed + const conditions = [ + resjson.response.error.errorcode == 100 + ].includes(true); + if (conditions) log.w('This error indicates a client problem.'); + return { valid: SteamAuthResult.Failure, res: resjson.response.error }; + } + + //log.d(JSON.stringify(resjson.response)); + if (resjson.response.params) { + // since rec room is not eligible for family sharing on Steam + const valid = resjson.response.params.steamid === userid && resjson.response.params.ownersteamid === userid; + if (valid) return { valid: SteamAuthResult.Success, res: resjson.response.params } + else throw new Error('`ownersteamid` is not equal to `steamid`, report me to GC devs!'); + } + else { + log.w("Steam Authentication failed: Steam response did not contain params or error! This should never be logged!"); + return { valid: SteamAuthResult.Failure, res: { errorcode: -1, errordesc: 'Steam response error' } }; + } + } catch (err) { + log.w(`Steam Authentication failed: ${(err as Error).message}`); + return { valid: SteamAuthResult.Failure, res: { errorcode: -1, errordesc: 'Steam response error' } }; + } + } + +} + +const Steam = new SteamBase(); + +export default Steam; \ No newline at end of file diff --git a/src/data/users.ts b/src/data/users.ts index 0d5b8c0..f1b5fcf 100644 --- a/src/data/users.ts +++ b/src/data/users.ts @@ -17,24 +17,10 @@ along with this program. If not, see . */ import { Redis } from "../db.ts"; import * as JsonWebToken from "@gz/jwt"; -import { Config } from "../config.ts"; -import { Profile } from "./profiles.ts"; -import { TokenBaseFormat } from "../apiutils.ts"; - -type UserInitOptions = { - client_id: string; - pubkey: string; -}; - -export enum AuthType { - Game, - Web, -} - -export interface UserTokenFormat extends TokenBaseFormat { - sub: string; - typ: AuthType.Web; -} +import { Config } from "../config/config.ts"; +import { Profile } from "./profile/base/profiles.ts"; +import type { UserTokenFormat } from "./auth/TokenBaseFormat.ts"; +import { UserInitOptions, AuthType } from "./UserTypes.ts"; const config = Config.getConfig(); diff --git a/src/db.ts b/src/db.ts index 40273af..1a83bd8 100644 --- a/src/db.ts +++ b/src/db.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { Redis } from "ioredis"; -import * as Config from "./config.ts"; +import * as Config from "./config/config.ts"; import Logging from "@proxnet/undead-logging"; import chalk from "npm:chalk@^5.3.0"; @@ -110,6 +110,9 @@ export const KeyGroups = { Root: "room", PlayerDorms: "player-dormids" }, + Subrooms: { + Root: "subroom" + }, Operators: "operators", Users: { Root: "users", diff --git a/src/discord.ts b/src/discord.ts deleted file mode 100644 index 940224d..0000000 --- a/src/discord.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* Galvanic Corrosion - Rec Room custom server for communities. - -Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . */ - -import * as discord from "discord.js"; -import { Config } from "./config.ts"; -import Logging from "@proxnet/undead-logging"; - -const log = new Logging("Discord"); - -const config = Config.getConfig(); -if (typeof config == "undefined") { - log.e(`Cannot start: Discord configuration is unavailable`); - Deno.exit(1); -} - -export const client = new discord.Client({ - intents: [ - discord.GatewayIntentBits.Guilds, - discord.GatewayIntentBits.GuildPresences, - ], -}); - -client.once(discord.Events.ClientReady, (client) => { - log.i(`Logged in to Discord as "${client.user.tag}"`); - client.user?.setActivity(config.public.motd, { - type: discord.ActivityType.Custom, - }); -}); - -let shuttingDown = false; -Deno.addSignalListener("SIGINT", () => { - if (client.readyTimestamp == null) return; - if (shuttingDown) return; - shuttingDown = true; - log.n("Disconnecting from Discord"); - client.destroy(); -}); - -export function login() { - if (config.discord?.token == Config.defaultConfig.discord?.token) { - log.i("Discord not configured, ignoring"); - return; - } - log.i(`Creating Discord connection..`); - client.login(config.discord?.token); -} - -export * as Discord from "./discord.ts"; diff --git a/src/main.ts b/src/main.ts index 169cb99..b72bfd7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,42 +16,44 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import * as Log from "@proxnet/undead-logging"; -import * as Config from "./config.ts"; +import * as Config from "./config/config.ts"; import { Database } from "./db.ts"; -import { APIUtils, ProfileTokenSchema } from "./apiutils.ts"; -import { Discord } from "./discord.ts"; -import { generateRandomString } from "./apiutils.ts"; +import { APIUtils } from "./apiutils.ts"; +import { ProfileTokenSchema } from "./data/auth/TokenSchema.ts"; +import { generateRandomString } from "./utils.ts"; import express from "express"; import { decode } from "@gz/jwt"; -import { ProfileTokenFormat } from "./data/profiles.ts"; import { SocketHandoff } from "./socket/handoff.ts"; import { SignalRSocketHandler } from "./socket/socket.ts"; -import { GameConfigs } from "./data/config.ts"; +import { GameConfigs } from "./data/config/GameConfigs.ts"; import { getVersion } from "./ver.ts"; -import Rooms from "./data/content/rooms.ts"; +import RoomsMisc from "./data/content/rooms/Rooms.ts"; import { PushNotificationId } from "./socket/types.ts"; -import Server from "./data/server.ts"; +import Server from "./data/server/server.ts"; +import type { ProfileTokenFormat } from "./data/auth/TokenBaseFormat.ts"; +import { addWatchdogListener } from "./watchdog.ts"; const instanceId = generateRandomString(64); const log = new Log.default("Main"); +Log.LoggingConfiguration.logTiming = Log.LogTiming.Deferred; + log.i(`Galvanic Corrosion '${await getVersion()}'`); const config = Config.getConfig(); -if (typeof config == "undefined") { - log.e("Cannot start: Configuration was not found."); - Deno.exit(5); -} -if (config.auth.secret == Config.defaultConfig.auth.secret) { - log.e(`Cannot start: Auth secret is default. Please change 'auth.secret' in 'config.json'`); - Deno.exit(5); -} -if (config.public.serverId == Config.defaultConfig.public.serverId) { - log.e(`Cannot start: Server ID is default. Please change 'public.serverId' in 'config.json'`); - Deno.exit(5); +function exitError(err: string, code?: number) { + log.e(`Cannot start: ${err}`); + if (code) Deno.exit(code); + else Deno.exit(5); } +if (typeof config == "undefined") + exitError("Configuration was not found."); +if (config.auth.secret == Config.defaultConfig.auth.secret) + exitError(`Auth secret is default. Please change 'auth.secret' in 'config.json'`); +if (config.public.serverId == Config.defaultConfig.public.serverId) + exitError(`Server ID is default. Please change 'public.serverId' in 'config.json'`); Log.MessageTypeVisibility.Network = config.logging.network; Log.MessageTypeVisibility.Debug = config.logging.debug; @@ -59,8 +61,7 @@ Log.MessageTypeVisibility.Debug = config.logging.debug; try { Database.connect(); } catch (err) { - log.e(`Cannot start: Redis could not be initialized. ${err}`); - Deno.exit(1); + exitError(`Redis could not be initialized. ${(err as Error).stack}`); } const app = express(); @@ -118,7 +119,7 @@ app.use((rq: express.Request, rs: express.Response) => { rs.json(APIUtils.genericResponseFormat(true, "Endpoint not found. Check your syntax and/or method.")); }); -if (!(await Rooms.generateBuiltinRooms())) log.i(`Generated built-in rooms`); +if (!(await RoomsMisc.generateBuiltinRooms())) log.i(`Generated built-in rooms`); await Server.CDN.ensureUserDirectory(); try { @@ -227,6 +228,7 @@ try { Deno.addSignalListener("SIGINT", () => { for (const socket of Server.UnifiedProfile.getAllSockets()) socket.sendNotification(PushNotificationId.ModerationQuitGame); // untested }); + addWatchdogListener(); if (!(await Server.UnifiedProfile.existsByName("Coach"))) Server.UnifiedProfile.create({ username: "Coach", id: 1 }); // create Coach id 1 if they do not exist if (!(await Server.UnifiedProfile.existsByName("Server"))) Server.UnifiedProfile.create({ username: "Server", id: 2 }); // create Server id 2 if they do not exist @@ -245,6 +247,4 @@ try { } catch (err) { log.e(`Cannot start: Network could not be initalized. ${err}`); Deno.exit(1); -} - -Discord.login(); \ No newline at end of file +} \ No newline at end of file diff --git a/src/routes/account/account.ts b/src/routes/account/account.ts index c5f5035..2e00fda 100644 --- a/src/routes/account/account.ts +++ b/src/routes/account/account.ts @@ -17,10 +17,10 @@ along with this program. If not, see . */ import { APIUtils, NoBody } from "../../apiutils.ts"; import express from "express"; -import { Profile } from "../../data/profiles.ts"; +import { Profile } from "../../data/profile/base/profiles.ts"; import { z } from "zod"; -import { AuthType } from "../../data/users.ts"; -import Server from "../../data/server.ts"; +import { AuthType } from "../../data/UserTypes.ts"; +import Server from "../../data/server/server.ts"; export const route = APIUtils.createRouter("/account"); diff --git a/src/routes/account/parentalcontrol.ts b/src/routes/account/parentalcontrol.ts index 590df79..a0123c5 100644 --- a/src/routes/account/parentalcontrol.ts +++ b/src/routes/account/parentalcontrol.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; export const route = APIUtils.createRouter('/parentalcontrol'); diff --git a/src/routes/api/announcement.ts b/src/routes/api/announcement.ts index 45ae93b..083c01f 100644 --- a/src/routes/api/announcement.ts +++ b/src/routes/api/announcement.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; export const route = APIUtils.createRouter("/announcement"); diff --git a/src/routes/api/avatar.ts b/src/routes/api/avatar.ts index cc4cfc0..2fea6c7 100644 --- a/src/routes/api/avatar.ts +++ b/src/routes/api/avatar.ts @@ -17,7 +17,7 @@ along with this program. If not, see . */ import { z } from "zod"; import { APIUtils, NoBody } from "../../apiutils.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; import express from "express"; import { AvatarSettings } from "../../data/profile/avatar.ts"; diff --git a/src/routes/api/challenge.ts b/src/routes/api/challenge.ts index 67140a1..0f2ac87 100644 --- a/src/routes/api/challenge.ts +++ b/src/routes/api/challenge.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; export const route = APIUtils.createRouter("/challenge"); diff --git a/src/routes/api/checklist.ts b/src/routes/api/checklist.ts index dc09ec4..ac6fcd7 100644 --- a/src/routes/api/checklist.ts +++ b/src/routes/api/checklist.ts @@ -16,8 +16,8 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; -import { ObjectiveType } from "../../data/objectives.ts"; -import { AuthType } from "../../data/users.ts"; +import { ObjectiveType } from "../../data/content/ObjectiveTypes.ts"; +import { AuthType } from "../../data/UserTypes.ts"; export const route = APIUtils.createRouter('/checklist'); diff --git a/src/routes/api/communityboard.ts b/src/routes/api/communityboard.ts index 6e80c3f..7db6415 100644 --- a/src/routes/api/communityboard.ts +++ b/src/routes/api/communityboard.ts @@ -16,8 +16,8 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; -import { Config } from "../../config.ts"; -import { AuthType } from "../../data/users.ts"; +import { Config } from "../../config/config.ts"; +import { AuthType } from "../../data/UserTypes.ts"; const config = Config.getConfig(); diff --git a/src/routes/api/config.ts b/src/routes/api/config.ts index a939084..6eef47d 100644 --- a/src/routes/api/config.ts +++ b/src/routes/api/config.ts @@ -16,14 +16,14 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; -import { GameConfigs } from "../../data/config.ts"; +import { getPublicConfig } from "../../data/config/PublicConfig.ts"; export const route = APIUtils.createRouter("/config"); const rateLimit = new APIUtils.RateLimiter(); route.router.get("/v2", rateLimit.middle(), (_rq, rs) => { - const config = GameConfigs.getConfig(); + const config = getPublicConfig(); if (config == null) rs.sendStatus(500); else rs.json(config); }); diff --git a/src/routes/api/consumables.ts b/src/routes/api/consumables.ts index 339358c..464b787 100644 --- a/src/routes/api/consumables.ts +++ b/src/routes/api/consumables.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; export const route = APIUtils.createRouter('/consumables'); diff --git a/src/routes/api/equipment.ts b/src/routes/api/equipment.ts index 601c726..bdfc43d 100644 --- a/src/routes/api/equipment.ts +++ b/src/routes/api/equipment.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; export const route = APIUtils.createRouter('/equipment'); diff --git a/src/routes/api/gameconfigs.ts b/src/routes/api/gameconfigs.ts index a442247..7b8f50b 100644 --- a/src/routes/api/gameconfigs.ts +++ b/src/routes/api/gameconfigs.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; -import { GameConfigs } from "../../data/config.ts"; +import { GameConfigs } from "../../data/config/GameConfigs.ts"; export const route = APIUtils.createRouter("/gameconfigs"); diff --git a/src/routes/api/images.ts b/src/routes/api/images.ts index fb9f50d..b2f3881 100644 --- a/src/routes/api/images.ts +++ b/src/routes/api/images.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; export const route = APIUtils.createRouter('/images'); @@ -27,4 +27,17 @@ route.router.get('/v2/named', APIUtils.emptyArrayResponse +); + +route.router.post('/v4/uploadsaved', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + APIUtils.requestDebug, + + (_rq, rs) => { + rs.json({ImageName:"notsaved.png"}); + }, + ); \ No newline at end of file diff --git a/src/routes/api/objectives.ts b/src/routes/api/objectives.ts index 766188c..aa96559 100644 --- a/src/routes/api/objectives.ts +++ b/src/routes/api/objectives.ts @@ -17,7 +17,7 @@ along with this program. If not, see . */ import { z } from "zod"; import { APIUtils, NoBody } from "../../apiutils.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; import express from "express"; export const route = APIUtils.createRouter('/objectives'); diff --git a/src/routes/api/playerReputation.ts b/src/routes/api/playerReputation.ts index ea67530..c7f5d71 100644 --- a/src/routes/api/playerReputation.ts +++ b/src/routes/api/playerReputation.ts @@ -17,9 +17,9 @@ along with this program. If not, see . */ import { z } from "zod"; import { APIUtils, NoBody } from "../../apiutils.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; import express from "express"; -import Server from "../../data/server.ts"; +import Server from "../../data/server/server.ts"; export const route = APIUtils.createRouter("/playerReputation"); diff --git a/src/routes/api/playerevents.ts b/src/routes/api/playerevents.ts index 116fa39..38003cf 100644 --- a/src/routes/api/playerevents.ts +++ b/src/routes/api/playerevents.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; export const route = APIUtils.createRouter('/playerevents'); diff --git a/src/routes/api/players.ts b/src/routes/api/players.ts index 3a62dd7..303c0a0 100644 --- a/src/routes/api/players.ts +++ b/src/routes/api/players.ts @@ -18,9 +18,9 @@ along with this program. If not, see . */ import Logging from "@proxnet/undead-logging"; import { APIUtils, NoBody } from "../../apiutils.ts"; import express from "express"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; import { z } from "zod"; -import Server from "../../data/server.ts"; +import Server from "../../data/server/server.ts"; const log = new Logging("ProgressionRoute"); diff --git a/src/routes/api/quickplay.ts b/src/routes/api/quickplay.ts index a897ae0..80daab4 100644 --- a/src/routes/api/quickplay.ts +++ b/src/routes/api/quickplay.ts @@ -16,8 +16,8 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; -import { Config } from "../../config.ts"; -import { AuthType } from "../../data/users.ts"; +import { Config } from "../../config/config.ts"; +import { AuthType } from "../../data/UserTypes.ts"; export const route = APIUtils.createRouter("/quickPlay"); diff --git a/src/routes/api/relationships.ts b/src/routes/api/relationships.ts index fde0d68..8264277 100644 --- a/src/routes/api/relationships.ts +++ b/src/routes/api/relationships.ts @@ -17,7 +17,7 @@ along with this program. If not, see . */ import { z } from "zod"; import { APIUtils, NoBody } from "../../apiutils.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; import express from "express"; export const route = APIUtils.createRouter("/relationships"); diff --git a/src/routes/api/rooms.ts b/src/routes/api/rooms.ts index 139db41..7d07543 100644 --- a/src/routes/api/rooms.ts +++ b/src/routes/api/rooms.ts @@ -17,13 +17,14 @@ along with this program. If not, see . */ import { z } from "zod"; import { APIUtils, NoBody } from "../../apiutils.ts"; -import Rooms from "../../data/content/rooms.ts"; -import { FactoryMode, RoomDataTypes, WriteMode } from "../../data/content/rooms/DataTypes.ts"; -import { AuthType } from "../../data/users.ts"; +import RoomsMisc from "../../data/content/rooms/Rooms.ts"; +import { FactoryMode, RoomDataTypes, WriteMode } from "../../data/content/rooms/base/DataTypes.ts"; +import { AuthType } from "../../data/UserTypes.ts"; import express from "express"; import { RoomFactory } from "../../data/content/rooms/RoomFactory.ts"; import { SubroomFactory } from "../../data/content/rooms/SubroomFactory.ts"; import Logging from "@proxnet/undead-logging"; +import Rooms from "../../data/content/rooms/base/RoomsBase.ts"; const log = new Logging("RoomsRoute"); @@ -83,7 +84,7 @@ route.router.get('/v1/hot', async (_rq, rs) => { // temporary: return all public AG rooms for testing - const rooms = await Rooms.getAllBuiltinRoomGenerations(); + const rooms = await RoomsMisc.getAllBuiltinRoomGenerations(); rs.json(rooms.map(room => room.Room).filter(room => room.Accessibility == RoomDataTypes.RoomAccessibility.Public)); }, @@ -95,7 +96,7 @@ route.router.get('/v2/baserooms', APIUtils.AuthenticationType(AuthType.Game), async (_rq, rs) => { - const rooms = await Rooms.getAllBuiltinRoomGenerations(); + const rooms = await RoomsMisc.getAllBuiltinRoomGenerations(); rs.json(rooms.map(room => room.Room).filter(room => room.CloningAllowed)); }, @@ -120,7 +121,7 @@ route.router.get('/v2/name/:name', rs.json(room.Room); return; } else if (rq.params.name == 'DormRoom') { - const dorm = await Rooms.getProfileDormDefault(rs.locals.profile); + const dorm = await RoomsMisc.getProfileDormDefault(rs.locals.profile); if (dorm) rs.json(dorm.Room); else rs.sendStatus(404); return; @@ -150,7 +151,7 @@ route.router.get('/v1/agRoomIds', async (_rq, rs) => { - const rooms = await Rooms.getAllBuiltinRoomGenerations(); + const rooms = await RoomsMisc.getAllBuiltinRoomGenerations(); rs.json(rooms.map(det => det.Room.RoomId)); }, @@ -175,7 +176,7 @@ route.router.post('/v1/clone', async (rq: express.Request, rs: express.Response) => { - const room = await Rooms.cloneRoom(rq.body.RoomId, rq.body.Name, rs.locals.profile); + const room = await RoomsMisc.cloneRoom(rq.body.RoomId, rq.body.Name, rs.locals.profile); const masterRoomFactory = await new RoomFactory({ id: rq.body.RoomId }).init(); @@ -231,7 +232,6 @@ route.router.post('/v4/saveData', } const subroomFactory = await new SubroomFactory({ - roomId: currentInstance.roomId, subroomId: rq.body.RoomSceneId, factoryMode: FactoryMode.Write, writeMode: WriteMode.Overwrite @@ -249,10 +249,6 @@ route.router.post('/v4/saveData', await subroomFactory.write(); rs.json(subroomFactory.export()); - - currentInstance.dataBlob = newFilename; - currentInstance.updatePlayers(); - Rooms.socketUpdateRoom(currentInstance); } }, diff --git a/src/routes/api/settings.ts b/src/routes/api/settings.ts index e0893b0..5775cc9 100644 --- a/src/routes/api/settings.ts +++ b/src/routes/api/settings.ts @@ -17,7 +17,7 @@ along with this program. If not, see . */ import Logging from "@proxnet/undead-logging"; import { APIUtils, NoBody } from "../../apiutils.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; import z from "zod"; import express from "express"; diff --git a/src/routes/api/storefronts.ts b/src/routes/api/storefronts.ts index 4353b6d..a668ccd 100644 --- a/src/routes/api/storefronts.ts +++ b/src/routes/api/storefronts.ts @@ -17,7 +17,7 @@ along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; import express from "express"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; import { StorefrontBalanceType } from "../../data/content/storefronts.ts"; export const route = APIUtils.createRouter('/storefronts'); diff --git a/src/routes/auth/account.ts b/src/routes/auth/account.ts index b92e768..9cea42c 100644 --- a/src/routes/auth/account.ts +++ b/src/routes/auth/account.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; export const route = APIUtils.createRouter("/account"); diff --git a/src/routes/auth/cachedlogin.ts b/src/routes/auth/cachedlogin.ts index 728eca8..5331042 100644 --- a/src/routes/auth/cachedlogin.ts +++ b/src/routes/auth/cachedlogin.ts @@ -16,7 +16,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; export const route = APIUtils.createRouter("/cachedlogin"); diff --git a/src/routes/auth/connect.ts b/src/routes/auth/connect.ts index 829f2b8..f14df58 100644 --- a/src/routes/auth/connect.ts +++ b/src/routes/auth/connect.ts @@ -18,14 +18,16 @@ along with this program. If not, see . */ import { APIUtils, NoBody } from "../../apiutils.ts"; import express from "express"; import { decode } from "@gz/jwt"; -import { Config } from "../../config.ts"; +import { Config } from "../../config/config.ts"; import Logging from "@proxnet/undead-logging"; import { z } from "zod"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; import { Redis } from "../../db.ts"; import { validVersions } from "../api/versioncheck.ts"; -import { Steam } from "../../data/steam.ts"; -import Server from "../../data/server.ts"; +import Steam from "../../data/steam/steam.ts"; +import Server from "../../data/server/server.ts"; +import { DeviceClass } from "../../data/live/types.ts"; +import { SteamAuthResult } from "../../data/steam/SteamAuthTypes.ts"; const config = Config.getConfig(); @@ -124,7 +126,7 @@ route.router.post("/token", }); } - const conditionsMet = ![ + const conditions = [ rq.body.client_id === "recroom", rq.body.platform === "0", validVersions.includes(rq.body.ver), @@ -136,7 +138,8 @@ route.router.post("/token", !(rq.body.time.length > 32), !(rq.body.asid.length > 32), SteamPlatformParamsSchema.safeParse(JSON.parse(rq.body.platform_auth)).success - ].includes(false); + ]; + const conditionsMet = !conditions.includes(false); if (!conditionsMet) { requestFailed(); @@ -164,48 +167,56 @@ route.router.post("/token", targetAccount = parseInt(decodedToken.sub ? decodedToken.sub : "NaN"); } - + const platformAuth = (JSON.parse(rq.body.platform_auth)) as SteamPlatformParams; + let platformid: string | null; if (config.auth.steamkey) { - const steamAuthed = await Steam.AuthenticateUserTicket(platformAuth.Ticket, rq.body.platform_id); - if (!steamAuthed) { + const steamAuth = await Steam.AuthenticateUserTicket(platformAuth.Ticket, rq.body.platform_id); + if (steamAuth.valid == SteamAuthResult.Failure) { requestFailed(); return; - } - } + } else if (steamAuth.valid == SteamAuthResult.Success) platformid = steamAuth.res.steamid; + else platformid = null; + } else platformid = null; if (isNaN(targetAccount)) { requestFailed(); return; } - + const accounts = await rs.locals.user.getAssociatedProfiles(); if (!accounts.has(targetAccount)) { requestFailed("access_denied"); return; } - + rs.locals.user.addAssociatedDeviceId(rq.body.device_id); - rs.locals.user.addAssociatedPlatformId(rq.body.platform_id); - Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, rq.body.platform_id), targetAccount); - + if (platformid) rs.locals.user.addAssociatedPlatformId(platformid); + if (platformid) Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, platformid), targetAccount); + const profile = Server.UnifiedProfile.get(targetAccount); if (!profile) { requestFailed(); return; } + const deviceClass = Number(rq.body.device_class); + if (typeof DeviceClass[deviceClass] == 'undefined') { + requestFailed(); + return; + } + const details = await profile.export(); log.i(`Player ${details?.username} "${details?.displayName}" (${profile.getId()}) logged in`); - const token = await profile.getToken(); + const token = await profile.Auth.getToken(); rs.json({ access_token: token, refresh_token: token, }); - await profile.setKnownDeviceClass(Number(rq.body.device_class)); + await profile.setKnownDeviceClass(deviceClass); nxt(); }, diff --git a/src/routes/cdn.ts b/src/routes/cdn.ts index 1c5809a..698d50b 100644 --- a/src/routes/cdn.ts +++ b/src/routes/cdn.ts @@ -17,8 +17,8 @@ along with this program. If not, see . */ import { APIUtils } from "../apiutils.ts"; import { File } from "../data/content/cdn.ts"; -import Server from "../data/server.ts"; -import { AuthType } from "../data/users.ts"; +import Server from "../data/server/server.ts"; +import { AuthType } from "../data/UserTypes.ts"; import { route as ConfigRoute } from "./cdn/config.ts"; import express from "express"; import { Buffer } from "node:buffer"; diff --git a/src/routes/match/goto.ts b/src/routes/match/goto.ts index f4d3397..1dc0f05 100644 --- a/src/routes/match/goto.ts +++ b/src/routes/match/goto.ts @@ -19,7 +19,7 @@ import Logging from "@proxnet/undead-logging"; import { APIUtils, NoBody } from "../../apiutils.ts"; import Matchmaking from "../../data/live/base.ts"; import { MatchmakingErrorCode } from "../../data/live/types.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; import express from "express"; import { z } from "zod"; @@ -33,10 +33,10 @@ interface MatchmakingParams { } const ProperCaseBooleanSchema = z.preprocess((val) => { - if (val === "True") return true; - if (val === "False") return false; - if (typeof val === "boolean") return val; // allow raw booleans too - return val; // will fail validation + if (val === "True") return true; + if (val === "False") return false; + if (typeof val === "boolean") return val; + return val; }, z.boolean()); interface MatchmakingOptions { diff --git a/src/routes/match/player.ts b/src/routes/match/player.ts index e773ebe..fbfe3ca 100644 --- a/src/routes/match/player.ts +++ b/src/routes/match/player.ts @@ -20,10 +20,10 @@ import { APIUtils, NoBody } from "../../apiutils.ts"; import express from "express"; import Matchmaking from "../../data/live/base.ts"; import Presence, { PresenceExport } from "../../data/live/presence.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; import { PlayerStatusVisibility, VRMovementMode } from "../../data/live/types.ts"; import { SettingKey } from "../../data/content/settings.ts"; -import Server from "../../data/server.ts"; +import Server from "../../data/server/server.ts"; export const route = APIUtils.createRouter('/player'); diff --git a/src/routes/match/room.ts b/src/routes/match/room.ts index 974f3f0..0a859fb 100644 --- a/src/routes/match/room.ts +++ b/src/routes/match/room.ts @@ -17,7 +17,7 @@ along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; import Instances from "../../data/live/instances.ts"; -import { AuthType } from "../../data/users.ts"; +import { AuthType } from "../../data/UserTypes.ts"; import express from "express"; export const route = APIUtils.createRouter('/room'); diff --git a/src/routes/nameserver.ts b/src/routes/nameserver.ts index 9b0744a..0b578fe 100644 --- a/src/routes/nameserver.ts +++ b/src/routes/nameserver.ts @@ -16,9 +16,10 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../apiutils.ts"; -import { Config } from "../config.ts"; +import { Config } from "../config/config.ts"; +import type { GalvanicConfiguration } from "../config/GalvanicConfiguration.ts"; -const config = Config.getConfig() as Config.GalvanicConfiguration; +const config = Config.getConfig() as GalvanicConfiguration; const protocol = config.web.api.securepublichost ? "https" : "http"; export const route = APIUtils.createRouter("/ns"); diff --git a/src/routes/storage.ts b/src/routes/storage.ts index ef755b5..351c82d 100644 --- a/src/routes/storage.ts +++ b/src/routes/storage.ts @@ -17,12 +17,12 @@ along with this program. If not, see . */ import { z } from "zod"; import { APIUtils, NoBody } from "../apiutils.ts"; -import { AuthType } from "../data/users.ts"; import { Buffer } from "node:buffer"; import multer from "multer"; import { FileType } from "../data/content/cdn.ts"; import express from "express"; -import Server from "../data/server.ts"; +import { AuthType } from "../data/UserTypes.ts"; +import Server from "../data/server/server.ts"; export const route = APIUtils.createRouter("/storage"); diff --git a/src/routes/user.ts b/src/routes/user.ts index 04da4b4..b6417d8 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -18,12 +18,13 @@ along with this program. If not, see . */ import { APIUtils, getSrcIpDefault, NoBody } from "../apiutils.ts"; // @ts-types = "npm:@types/express" import express from "express"; -import { User, UserTokenFormat } from "../data/users.ts"; -import { Config } from "../config.ts"; +import { User } from "../data/users.ts"; +import { Config } from "../config/config.ts"; import crypto from "node:crypto"; import Logging from "@proxnet/undead-logging"; import { decode } from "@gz/jwt"; import z from "zod"; +import type { UserTokenFormat } from "../data/auth/TokenBaseFormat.ts"; const log = new Logging("UserRoute"); diff --git a/src/socket/route.ts b/src/socket/route.ts index 5c7f5a6..53e8d5d 100644 --- a/src/socket/route.ts +++ b/src/socket/route.ts @@ -16,8 +16,8 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../apiutils.ts"; -import { Config } from "../config.ts"; -import { AuthType } from "../data/users.ts"; +import { Config } from "../config/config.ts"; +import { AuthType } from "../data/UserTypes.ts"; import { SocketHandoff } from "./handoff.ts"; const config = Config.getConfig(); diff --git a/src/socket/socket.ts b/src/socket/socket.ts index 239772c..01ee7cd 100644 --- a/src/socket/socket.ts +++ b/src/socket/socket.ts @@ -15,7 +15,7 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { Profile } from "../data/profiles.ts"; +import type { Profile } from "../data/profile/base/profiles.ts"; import Logging from "@proxnet/undead-logging"; import { CompletionMessage, @@ -34,9 +34,8 @@ import { import { SocketTarget } from "./targets/targetbase.ts"; import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts"; import Presence from "../data/live/presence.ts"; -import Matchmaking from "../data/live/base.ts"; -const logmessages = false; +const logmessages = true; export class SignalRSocketHandler { @@ -62,13 +61,11 @@ export class SignalRSocketHandler { this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget(this)); + for (const target of this.#Targets.values()) target.onInit(); + this.#PeriodicalId = setInterval(async () => { if (this.#killed) return; - if (this.#socket.readyState !== this.#socket.CLOSED) { - const pres = await Presence.get(this.#profile); - this.sendNotification("PresenceUpdate", await pres.export()); - this.sendRaw({ type: 6 }); - } + if (this.#socket.readyState == this.#socket.OPEN) await this.presenceUpdate(); }, 8000); this.#socket.onclose = (ev) => { @@ -77,6 +74,12 @@ export class SignalRSocketHandler { } + async presenceUpdate() { + const pres = await Presence.get(this.#profile); + this.sendNotification("PresenceUpdate", await pres.export()); + this.sendRaw({ type: 6 }); + } + async #dispatchTarget(target: string, args: unknown): Promise { if (this.#killed) { const error = "Tried to dispatch socket target on dead socket"; @@ -161,19 +164,24 @@ export class SignalRSocketHandler { this.#socket.addEventListener('close', this.destroy(this, true)); } - destroy(sock: SignalRSocketHandler, internal: boolean | undefined = false) { - return () => { - sock.#killed = true; - clearInterval(sock.#PeriodicalId); - sock.sendRaw({ type: 7, error: "Socket closed" }); - if (!internal) sock.#socket.close(); - sock.#log.i(`Closed socket`); - sock.#profile.clearSocketHandler(); + destroy(handler: SignalRSocketHandler, internal: boolean | undefined = false) { + return (ev: CloseEvent) => { + handler.#killed = true; + clearInterval(handler.#PeriodicalId); - for (const target of sock.#Targets.values()) target.destroy(); + let errorReason = "Socket closed by server"; + this.#log.d(`Socket close code: ${ev.code}`); + if (ev.reason.includes('Bye!')) errorReason = "Socket closed by client request"; + + handler.sendRaw({ type: 7, error: errorReason }); + + if (!internal) handler.#socket.close(); + handler.#log.i(`Closed socket`); + handler.#profile.clearSocketHandler(); + + for (const target of handler.#Targets.values()) target.onDestroy(); this.#profile.getInstance()?.removePlayer(this.#profile); - Matchmaking.deleteLoginLock(this.#profile); } } diff --git a/src/socket/targets/SubscribeToPlayers.ts b/src/socket/targets/SubscribeToPlayers.ts index f94fb47..18d7cf3 100644 --- a/src/socket/targets/SubscribeToPlayers.ts +++ b/src/socket/targets/SubscribeToPlayers.ts @@ -17,10 +17,10 @@ along with this program. If not, see . */ import { z } from "zod"; import { SocketTarget } from "./targetbase.ts"; -import { SelfAccountExport } from "../../data/profiles.ts"; -import { ProfileEvents, ProfileUpdatedEvent } from "../../data/profileevents.ts"; +import type { Profile } from "../../data/profile/base/profiles.ts"; +import type { ProfileUpdatedEvent } from "../../data/profileevents.ts"; import { PushNotificationId } from "../types.ts"; -import Server from "../../data/server.ts"; +import Server from "../../data/server/server.ts"; const ArgumentSchema = z.object({ PlayerIds: z.array(z.number()) @@ -28,46 +28,30 @@ const ArgumentSchema = z.object({ export class PlayerSocketSubscriptionTarget extends SocketTarget { - updateSocket(profile: SelfAccountExport) { - this.socket.sendNotification(PushNotificationId.SubscriptionUpdateProfile, profile); + async updateSocket(profile: Profile) { + this.socket.sendNotification(PushNotificationId.SubscriptionUpdateProfile, await profile.export() || undefined); } - subscriptions: { id: number, callback: (ev: unknown) => void }[] = []; - - setSubscriptions(subs: number[]) { - - this.clearSubscriptions(); - - for (const id of subs) { - const profile = Server.UnifiedProfile.get(id); - if (!profile) continue; - this.subscriptions.push({ id: id, callback: profile - .on(ProfileEvents.BaseUpdated, (async ev => { - const exported = await ev.profile.export(); - if (exported) this.updateSocket(exported); - } - )) }); - } + subscriptions: number[] = []; + #callback = (ev: ProfileUpdatedEvent) => { + if (this.subscriptions.includes(ev.profile.getId())) + this.updateSocket(ev.profile); } - clearSubscriptions() { - for (const sub of this.subscriptions) { - const profile = Server.UnifiedProfile.get(sub.id); - if (profile) - profile.off(ProfileEvents.BaseUpdated, sub.callback); - } + override onInit() { + Server.on('profile.updated', this.#callback); } - override destroy() { - this.clearSubscriptions(); + override onDestroy() { + Server.off('profile.updated', this.#callback); } // deno-lint-ignore require-await override async exec(args: unknown) { const parsed = ArgumentSchema.safeParse(args); if (parsed.success) { - this.setSubscriptions(parsed.data.PlayerIds); + this.subscriptions = parsed.data.PlayerIds; return; } else throw new Error("Invalid arguments"); } diff --git a/src/socket/targets/targetbase.ts b/src/socket/targets/targetbase.ts index 9dedfdb..e4949c8 100644 --- a/src/socket/targets/targetbase.ts +++ b/src/socket/targets/targetbase.ts @@ -15,7 +15,7 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { SignalRSocketHandler } from "../socket.ts"; +import type { SignalRSocketHandler } from "../socket.ts"; export class SocketTarget { @@ -25,7 +25,11 @@ export class SocketTarget { this.socket = socket; } - destroy() { + onInit() { + return; + } + + onDestroy() { return; } diff --git a/src/types/express.ts b/src/types/express.ts index 9ab4dec..c9b648e 100644 --- a/src/types/express.ts +++ b/src/types/express.ts @@ -15,7 +15,7 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { Profile } from "../data/profiles.ts"; +import { Profile } from "../data/profile/base/profiles.ts"; import { User } from "../data/users.ts"; declare global { diff --git a/src/types/http.ts b/src/types/http.ts index 5b2908e..d63ccf2 100644 --- a/src/types/http.ts +++ b/src/types/http.ts @@ -15,7 +15,7 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -import { ProfileTokenFormat } from "../data/profiles.ts"; +import type { ProfileTokenFormat } from "../data/auth/TokenBaseFormat.ts"; declare module 'node:http' { interface IncomingMessage { diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..de5290d --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,28 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +export function generateRandomString(length: number) { + const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let randomString = ""; + + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + randomString += characters.charAt(randomIndex); + } + + return randomString; +} \ No newline at end of file diff --git a/src/watchdog.ts b/src/watchdog.ts new file mode 100644 index 0000000..2aa62d7 --- /dev/null +++ b/src/watchdog.ts @@ -0,0 +1,34 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import Logging, { LoggingConfiguration, LogTiming } from "@proxnet/undead-logging"; + +const log = new Logging("Watchdog"); + +let added = false; +export function addWatchdogListener() { + if (added) return; + added = true; + Deno.addSignalListener('SIGINT', () => { + LoggingConfiguration.logTiming = LogTiming.Sync; + + setTimeout(() => { + log.e(`Server took too long (60s) to shut down! Exiting.`); + Deno.exit(2); + }, 60000); + }); +} \ No newline at end of file