Many changes. Commit before I break down.
- Authentication middleware uses Zod - PhotonRegionId in config - DB key changes and additions - WebSocket for SignalR mock - Presence additions * Needs modification for playerIds (do not store `Profile` in a set, this will cause sync issues) - Profile settings - Profile Device Class - Zod properly checks for issuer in token - Room scene type bug - Setting key import started - Instancing changes - PlayerReporting API route - Deduplicated auth/connect/token - match/player/login begin - WebSocket hands off connection to SignalR handler
This commit is contained in:
@@ -9,14 +9,17 @@
|
|||||||
"@gz/jwt": "jsr:@gz/jwt@^0.1.0",
|
"@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.2.0",
|
||||||
"@std/assert": "jsr:@std/assert@1",
|
"@std/assert": "jsr:@std/assert@1",
|
||||||
|
"@std/http": "jsr:@std/http@^1.0.13",
|
||||||
"@types/cookie-parser": "npm:@types/cookie-parser@^1.4.8",
|
"@types/cookie-parser": "npm:@types/cookie-parser@^1.4.8",
|
||||||
"@types/express": "npm:@types/express@^5.0.0",
|
"@types/express": "npm:@types/express@^5.0.0",
|
||||||
"@types/validator": "npm:@types/validator@^13.12.2",
|
"@types/validator": "npm:@types/validator@^13.12.2",
|
||||||
|
"@types/ws": "npm:@types/ws@^8.18.0",
|
||||||
"cookie-parser": "npm:cookie-parser@^1.4.7",
|
"cookie-parser": "npm:cookie-parser@^1.4.7",
|
||||||
"discord.js": "npm:discord.js@^14.16.3",
|
"discord.js": "npm:discord.js@^14.16.3",
|
||||||
"express": "npm:express@^4.21.2",
|
"express": "npm:express@^4.21.2",
|
||||||
"ioredis": "npm:ioredis@^5.5.0",
|
"ioredis": "npm:ioredis@^5.5.0",
|
||||||
"validator": "npm:validator@^13.12.0",
|
"validator": "npm:validator@^13.12.0",
|
||||||
|
"ws": "npm:ws@^8.18.1",
|
||||||
"zod": "npm:zod@^3.24.2"
|
"zod": "npm:zod@^3.24.2"
|
||||||
},
|
},
|
||||||
"files": [],
|
"files": [],
|
||||||
|
|||||||
87
deno.lock
generated
87
deno.lock
generated
@@ -5,8 +5,17 @@
|
|||||||
"jsr:@proxnet/undead-logging@^1.2.0": "1.2.0",
|
"jsr:@proxnet/undead-logging@^1.2.0": "1.2.0",
|
||||||
"jsr:@std/assert@1": "1.0.7",
|
"jsr:@std/assert@1": "1.0.7",
|
||||||
"jsr:@std/bytes@^1.0.2": "1.0.4",
|
"jsr:@std/bytes@^1.0.2": "1.0.4",
|
||||||
|
"jsr:@std/cli@^1.0.12": "1.0.15",
|
||||||
"jsr:@std/crypto@^1.0.3": "1.0.3",
|
"jsr:@std/crypto@^1.0.3": "1.0.3",
|
||||||
|
"jsr:@std/encoding@^1.0.7": "1.0.8",
|
||||||
|
"jsr:@std/fmt@^1.0.5": "1.0.6",
|
||||||
|
"jsr:@std/html@^1.0.3": "1.0.3",
|
||||||
|
"jsr:@std/http@^1.0.13": "1.0.13",
|
||||||
"jsr:@std/internal@^1.0.5": "1.0.5",
|
"jsr:@std/internal@^1.0.5": "1.0.5",
|
||||||
|
"jsr:@std/media-types@^1.1.0": "1.1.0",
|
||||||
|
"jsr:@std/net@^1.0.4": "1.0.4",
|
||||||
|
"jsr:@std/path@^1.0.8": "1.0.8",
|
||||||
|
"jsr:@std/streams@^1.0.9": "1.0.9",
|
||||||
"jsr:@std/uuid@*": "1.0.4",
|
"jsr:@std/uuid@*": "1.0.4",
|
||||||
"npm:@imagemagick/magick-wasm@0.0.31": "0.0.31",
|
"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_@types+express@5.0.0",
|
||||||
@@ -15,12 +24,14 @@
|
|||||||
"npm:@types/express@5": "5.0.0",
|
"npm:@types/express@5": "5.0.0",
|
||||||
"npm:@types/node@*": "22.5.4",
|
"npm:@types/node@*": "22.5.4",
|
||||||
"npm:@types/validator@^13.12.2": "13.12.2",
|
"npm:@types/validator@^13.12.2": "13.12.2",
|
||||||
|
"npm:@types/ws@^8.18.0": "8.18.0",
|
||||||
"npm:chalk@^5.3.0": "5.3.0",
|
"npm:chalk@^5.3.0": "5.3.0",
|
||||||
"npm:cookie-parser@^1.4.7": "1.4.7",
|
"npm:cookie-parser@^1.4.7": "1.4.7",
|
||||||
"npm:discord.js@^14.16.3": "14.16.3",
|
"npm:discord.js@^14.16.3": "14.16.3",
|
||||||
"npm:express@^4.21.2": "4.21.2",
|
"npm:express@^4.21.2": "4.21.2",
|
||||||
"npm:ioredis@^5.5.0": "5.5.0",
|
"npm:ioredis@^5.5.0": "5.5.0",
|
||||||
"npm:validator@^13.12.0": "13.12.0",
|
"npm:validator@^13.12.0": "13.12.0",
|
||||||
|
"npm:ws@^8.18.1": "8.18.1",
|
||||||
"npm:zod@^3.24.2": "3.24.2"
|
"npm:zod@^3.24.2": "3.24.2"
|
||||||
},
|
},
|
||||||
"jsr": {
|
"jsr": {
|
||||||
@@ -42,12 +53,49 @@
|
|||||||
"@std/bytes@1.0.4": {
|
"@std/bytes@1.0.4": {
|
||||||
"integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc"
|
"integrity": "11a0debe522707c95c7b7ef89b478c13fb1583a7cfb9a85674cd2cc2e3a28abc"
|
||||||
},
|
},
|
||||||
|
"@std/cli@1.0.15": {
|
||||||
|
"integrity": "e79ba3272ec710ca44d8342a7688e6288b0b88802703f3264184b52893d5e93f"
|
||||||
|
},
|
||||||
"@std/crypto@1.0.3": {
|
"@std/crypto@1.0.3": {
|
||||||
"integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f"
|
"integrity": "a2a32f51ddef632d299e3879cd027c630dcd4d1d9a5285d6e6788072f4e51e7f"
|
||||||
},
|
},
|
||||||
|
"@std/encoding@1.0.8": {
|
||||||
|
"integrity": "a6c8f3f933ab1bed66244f435d1dc0fd23a888e07195532122ddc3d5f8f0e6b4"
|
||||||
|
},
|
||||||
|
"@std/fmt@1.0.6": {
|
||||||
|
"integrity": "a2c56a69a2369876ddb3ad6a500bb6501b5bad47bb3ea16bfb0c18974d2661fc"
|
||||||
|
},
|
||||||
|
"@std/html@1.0.3": {
|
||||||
|
"integrity": "7a0ac35e050431fb49d44e61c8b8aac1ebd55937e0dc9ec6409aa4bab39a7988"
|
||||||
|
},
|
||||||
|
"@std/http@1.0.13": {
|
||||||
|
"integrity": "d29618b982f7ae44380111f7e5b43da59b15db64101198bb5f77100d44eb1e1e",
|
||||||
|
"dependencies": [
|
||||||
|
"jsr:@std/cli",
|
||||||
|
"jsr:@std/encoding",
|
||||||
|
"jsr:@std/fmt",
|
||||||
|
"jsr:@std/html",
|
||||||
|
"jsr:@std/media-types",
|
||||||
|
"jsr:@std/net",
|
||||||
|
"jsr:@std/path",
|
||||||
|
"jsr:@std/streams"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@std/internal@1.0.5": {
|
"@std/internal@1.0.5": {
|
||||||
"integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
|
"integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba"
|
||||||
},
|
},
|
||||||
|
"@std/media-types@1.1.0": {
|
||||||
|
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
|
||||||
|
},
|
||||||
|
"@std/net@1.0.4": {
|
||||||
|
"integrity": "2f403b455ebbccf83d8a027d29c5a9e3a2452fea39bb2da7f2c04af09c8bc852"
|
||||||
|
},
|
||||||
|
"@std/path@1.0.8": {
|
||||||
|
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
|
||||||
|
},
|
||||||
|
"@std/streams@1.0.9": {
|
||||||
|
"integrity": "a9d26b1988cdd7aa7b1f4b51e1c36c1557f3f252880fa6cc5b9f37078b1a5035"
|
||||||
|
},
|
||||||
"@std/uuid@1.0.4": {
|
"@std/uuid@1.0.4": {
|
||||||
"integrity": "f4233149cc8b4753cc3763fd83a7c4101699491f55c7be78dc7b30281946d7a0",
|
"integrity": "f4233149cc8b4753cc3763fd83a7c4101699491f55c7be78dc7b30281946d7a0",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -105,11 +153,11 @@
|
|||||||
"@discordjs/rest",
|
"@discordjs/rest",
|
||||||
"@discordjs/util",
|
"@discordjs/util",
|
||||||
"@sapphire/async-queue",
|
"@sapphire/async-queue",
|
||||||
"@types/ws",
|
"@types/ws@8.5.13",
|
||||||
"@vladfrangu/async_event_emitter",
|
"@vladfrangu/async_event_emitter",
|
||||||
"discord-api-types@0.37.83",
|
"discord-api-types@0.37.83",
|
||||||
"tslib",
|
"tslib",
|
||||||
"ws"
|
"ws@8.18.0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@imagemagick/magick-wasm@0.0.31": {
|
"@imagemagick/magick-wasm@0.0.31": {
|
||||||
@@ -135,13 +183,13 @@
|
|||||||
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/connect",
|
"@types/connect",
|
||||||
"@types/node"
|
"@types/node@22.5.4"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@types/connect@3.4.38": {
|
"@types/connect@3.4.38": {
|
||||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/node"
|
"@types/node@22.5.4"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@types/cookie-parser@1.4.8_@types+express@5.0.0": {
|
"@types/cookie-parser@1.4.8_@types+express@5.0.0": {
|
||||||
@@ -153,7 +201,7 @@
|
|||||||
"@types/express-serve-static-core@5.0.1": {
|
"@types/express-serve-static-core@5.0.1": {
|
||||||
"integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==",
|
"integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/node",
|
"@types/node@22.5.4",
|
||||||
"@types/qs",
|
"@types/qs",
|
||||||
"@types/range-parser",
|
"@types/range-parser",
|
||||||
"@types/send"
|
"@types/send"
|
||||||
@@ -174,10 +222,16 @@
|
|||||||
"@types/mime@1.3.5": {
|
"@types/mime@1.3.5": {
|
||||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
|
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
|
||||||
},
|
},
|
||||||
|
"@types/node@22.12.0": {
|
||||||
|
"integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
|
||||||
|
"dependencies": [
|
||||||
|
"undici-types@6.20.0"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@types/node@22.5.4": {
|
"@types/node@22.5.4": {
|
||||||
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
"integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"undici-types"
|
"undici-types@6.19.8"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@types/qs@6.9.17": {
|
"@types/qs@6.9.17": {
|
||||||
@@ -190,24 +244,30 @@
|
|||||||
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
|
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/mime",
|
"@types/mime",
|
||||||
"@types/node"
|
"@types/node@22.5.4"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@types/serve-static@1.15.7": {
|
"@types/serve-static@1.15.7": {
|
||||||
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
|
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/http-errors",
|
"@types/http-errors",
|
||||||
"@types/node",
|
"@types/node@22.5.4",
|
||||||
"@types/send"
|
"@types/send"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@types/validator@13.12.2": {
|
"@types/validator@13.12.2": {
|
||||||
"integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA=="
|
"integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA=="
|
||||||
},
|
},
|
||||||
|
"@types/ws@8.18.0": {
|
||||||
|
"integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==",
|
||||||
|
"dependencies": [
|
||||||
|
"@types/node@22.12.0"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@types/ws@8.5.13": {
|
"@types/ws@8.5.13": {
|
||||||
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
|
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/node"
|
"@types/node@22.5.4"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@vladfrangu/async_event_emitter@2.4.6": {
|
"@vladfrangu/async_event_emitter@2.4.6": {
|
||||||
@@ -694,6 +754,9 @@
|
|||||||
"undici-types@6.19.8": {
|
"undici-types@6.19.8": {
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||||
},
|
},
|
||||||
|
"undici-types@6.20.0": {
|
||||||
|
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
|
||||||
|
},
|
||||||
"undici@6.19.8": {
|
"undici@6.19.8": {
|
||||||
"integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g=="
|
"integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g=="
|
||||||
},
|
},
|
||||||
@@ -712,6 +775,9 @@
|
|||||||
"ws@8.18.0": {
|
"ws@8.18.0": {
|
||||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="
|
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="
|
||||||
},
|
},
|
||||||
|
"ws@8.18.1": {
|
||||||
|
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="
|
||||||
|
},
|
||||||
"zod@3.24.2": {
|
"zod@3.24.2": {
|
||||||
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="
|
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="
|
||||||
}
|
}
|
||||||
@@ -862,14 +928,17 @@
|
|||||||
"jsr:@gz/jwt@0.1",
|
"jsr:@gz/jwt@0.1",
|
||||||
"jsr:@proxnet/undead-logging@^1.2.0",
|
"jsr:@proxnet/undead-logging@^1.2.0",
|
||||||
"jsr:@std/assert@1",
|
"jsr:@std/assert@1",
|
||||||
|
"jsr:@std/http@^1.0.13",
|
||||||
"npm:@types/cookie-parser@^1.4.8",
|
"npm:@types/cookie-parser@^1.4.8",
|
||||||
"npm:@types/express@5",
|
"npm:@types/express@5",
|
||||||
"npm:@types/validator@^13.12.2",
|
"npm:@types/validator@^13.12.2",
|
||||||
|
"npm:@types/ws@^8.18.0",
|
||||||
"npm:cookie-parser@^1.4.7",
|
"npm:cookie-parser@^1.4.7",
|
||||||
"npm:discord.js@^14.16.3",
|
"npm:discord.js@^14.16.3",
|
||||||
"npm:express@^4.21.2",
|
"npm:express@^4.21.2",
|
||||||
"npm:ioredis@^5.5.0",
|
"npm:ioredis@^5.5.0",
|
||||||
"npm:validator@^13.12.0",
|
"npm:validator@^13.12.0",
|
||||||
|
"npm:ws@^8.18.1",
|
||||||
"npm:zod@^3.24.2"
|
"npm:zod@^3.24.2"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ type genericResponse = {
|
|||||||
data?: object
|
data?: object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateMask(...num: number[]) {
|
||||||
|
return num.reduce((sum, val) => sum + val, 0);
|
||||||
|
}
|
||||||
|
|
||||||
export function genericResponseFormat(
|
export function genericResponseFormat(
|
||||||
failure: boolean,
|
failure: boolean,
|
||||||
msg?: string,
|
msg?: string,
|
||||||
@@ -118,7 +122,7 @@ export function emptyArrayResponse(_rq: express.Request, rs: express.Response) {
|
|||||||
rs.json([]);
|
rs.json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSrcIpDefault(rq: express.Request) {
|
export function getSrcIpDefault(rq: express.Request): string {
|
||||||
const cfIp = rq.header("cf-connecting-ip");
|
const cfIp = rq.header("cf-connecting-ip");
|
||||||
if (cfIp !== undefined) return cfIp;
|
if (cfIp !== undefined) return cfIp;
|
||||||
|
|
||||||
@@ -208,6 +212,24 @@ export interface TokenBaseFormat {
|
|||||||
}
|
}
|
||||||
export type TokenFormat = UserTokenFormat | ProfileTokenFormat;
|
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(
|
export async function Authentication(
|
||||||
rq: express.Request,
|
rq: express.Request,
|
||||||
rs: express.Response,
|
rs: express.Response,
|
||||||
@@ -238,16 +260,15 @@ export async function Authentication(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decodedToken = await decode<TokenFormat>(
|
const decodedToken = await decode<TokenFormat>(token, config.auth.secret, {algorithm: "HS512"});
|
||||||
token,
|
const schemaResult = TokenSchema.safeParse(decodedToken);
|
||||||
config.auth.secret,
|
if (!schemaResult.success) {
|
||||||
{
|
returnUnauthorized();
|
||||||
algorithm: "HS512",
|
return;
|
||||||
},
|
}
|
||||||
);
|
|
||||||
|
|
||||||
const valid = ![
|
const valid = ![ // used to contain more conditions, now is only 1
|
||||||
decodedToken.iss == config.web.publichost,
|
decodedToken.iss == `${config.web.securepublichost ? 'https' : 'http'}://${config.web.publichost}`,
|
||||||
].includes(false);
|
].includes(false);
|
||||||
if (valid) {
|
if (valid) {
|
||||||
if (decodedToken.typ == AuthType.Web) rs.locals.user = new User(decodedToken.sub);
|
if (decodedToken.typ == AuthType.Web) rs.locals.user = new User(decodedToken.sub);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
|
import { PhotonRegionCodeString } from "./data/live/types.ts";
|
||||||
|
|
||||||
const log = new Logging("Config");
|
const log = new Logging("Config");
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ type PublicConfiguration = {
|
|||||||
levelScale: number;
|
levelScale: number;
|
||||||
maxLevels: number;
|
maxLevels: number;
|
||||||
patches: string[];
|
patches: string[];
|
||||||
|
photonRegionId: PhotonRegionCodeString;
|
||||||
};
|
};
|
||||||
|
|
||||||
type LoggingConfiguration = {
|
type LoggingConfiguration = {
|
||||||
@@ -79,6 +81,7 @@ export const defaultConfig: GalvanicConfiguration = {
|
|||||||
levelScale: 1,
|
levelScale: 1,
|
||||||
maxLevels: 30,
|
maxLevels: 30,
|
||||||
patches: [],
|
patches: [],
|
||||||
|
photonRegionId: PhotonRegionCodeString.UnitedStates,
|
||||||
},
|
},
|
||||||
logging: {
|
logging: {
|
||||||
notfound: false,
|
notfound: false,
|
||||||
|
|||||||
@@ -83,6 +83,16 @@ export interface Room {
|
|||||||
DisableMicAutoMute?: boolean
|
DisableMicAutoMute?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum RoomWarningMask {
|
||||||
|
None,
|
||||||
|
Scary,
|
||||||
|
Mature = 2,
|
||||||
|
FlashingLights = 4,
|
||||||
|
IntenseMotion = 8,
|
||||||
|
Violence = 16,
|
||||||
|
Custom = 32
|
||||||
|
}
|
||||||
|
|
||||||
export enum TagType {
|
export enum TagType {
|
||||||
General,
|
General,
|
||||||
Auto,
|
Auto,
|
||||||
@@ -109,7 +119,7 @@ export interface RoomScene {
|
|||||||
|
|
||||||
export interface RoomDetails {
|
export interface RoomDetails {
|
||||||
Room: Room,
|
Room: Room,
|
||||||
Scenes: RoomScene,
|
Scenes: RoomScene[],
|
||||||
CoOwners: number[],
|
CoOwners: number[],
|
||||||
InvitedCoOwners: number[],
|
InvitedCoOwners: number[],
|
||||||
Moderators?: number[],
|
Moderators?: number[],
|
||||||
|
|||||||
9
src/data/content/settings.ts
Normal file
9
src/data/content/settings.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export enum SettingKey {
|
||||||
|
PlayerStatusVisibility = "PlayerStatusVisibility",
|
||||||
|
VRMovementMode = "VR_MOVEMENT_MODE",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SettingDefault {
|
||||||
|
PlayerStatusVisibility = "1",
|
||||||
|
VRMovementMode = "0"
|
||||||
|
}
|
||||||
@@ -1,37 +1,13 @@
|
|||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import Profile from "../profiles.ts";
|
import Profile from "../profiles.ts";
|
||||||
import { IntegratedRoomScene, RoomDetails } from "../content/roomtypes.ts";
|
import { RoomInstance, InstanceOptions } from "./types.ts";
|
||||||
|
import { Config } from "../../config.ts";
|
||||||
|
|
||||||
const log = new Logging("Instances");
|
const log = new Logging("Instances");
|
||||||
|
|
||||||
export interface RoomInstance {
|
const config = Config.getConfig();
|
||||||
|
|
||||||
roomInstanceId: number,
|
|
||||||
roomId: number,
|
|
||||||
subRoomId: number,
|
|
||||||
location: IntegratedRoomScene,
|
|
||||||
dataBlob?: string,
|
|
||||||
eventId?: number,
|
|
||||||
photonRegionId: "us",
|
|
||||||
photonRoomId: string,
|
|
||||||
name?: string,
|
|
||||||
maxCapacity: number,
|
|
||||||
isFull: boolean,
|
|
||||||
isPrivate: boolean,
|
|
||||||
isInProgress: boolean
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InstanceOptions {
|
|
||||||
|
|
||||||
Room: RoomDetails,
|
|
||||||
SceneIndex?: number,
|
|
||||||
EventId?: number,
|
|
||||||
Name?: string,
|
|
||||||
Private?: boolean
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// `Profile` isn't synchronized. Fix this.
|
||||||
const instancePlayers: Map<RoomInstance, Set<Profile>> = new Map();
|
const instancePlayers: Map<RoomInstance, Set<Profile>> = new Map();
|
||||||
/**
|
/**
|
||||||
* `Map<roomId (number), RoomInstance>`
|
* `Map<roomId (number), RoomInstance>`
|
||||||
@@ -101,10 +77,55 @@ class InstancesBase {
|
|||||||
return this.getInstancePlayers(instance).size < instance.maxCapacity;
|
return this.getInstancePlayers(instance).size < instance.maxCapacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
createInstance(options: InstanceOptions) {
|
#generateUniqueInstanceId() {
|
||||||
// todo: use room data to create room instance
|
let newInstanceId = Math.round(Math.random() * Math.pow(2, 31))
|
||||||
|
const allInstances = this.getAllInstances();
|
||||||
|
while (Array.from(allInstances.values()).map(val => val.roomInstanceId).includes(newInstanceId)) newInstanceId = Math.round(Math.random() * Math.pow(2, 31));
|
||||||
|
return newInstanceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance with options.
|
||||||
|
*
|
||||||
|
* If `options.FirstPlayer` is not specified, the created instance will not contain any players and may be removed.
|
||||||
|
*
|
||||||
|
* If one is, the player will be automatically added to the instance and their `profile.getInstance()` will be synchronized.
|
||||||
|
*/
|
||||||
|
createInstance(options: InstanceOptions) {
|
||||||
|
|
||||||
|
const scene = options.Room.Scenes[options.SceneIndex];
|
||||||
|
const newId = this.#generateUniqueInstanceId();
|
||||||
|
if (!scene) throw new Error("The specified scene did not exist.");
|
||||||
|
|
||||||
|
const newInstance: RoomInstance = {
|
||||||
|
roomInstanceId: newId,
|
||||||
|
roomId: options.Room.Room.RoomId,
|
||||||
|
subRoomId: scene.RoomSceneId,
|
||||||
|
location: scene.RoomSceneLocationId,
|
||||||
|
dataBlob: scene.DataBlobName == "" ? undefined : scene.DataBlobName,
|
||||||
|
eventId: options.EventId,
|
||||||
|
photonRegionId: config.public.photonRegionId,
|
||||||
|
photonRoomId: `20191120-GC${newId}`,
|
||||||
|
name: scene.Name === "Home" ? `^${options.Room.Room.Name}` : `^${options.Room.Room.Name}.${scene.Name}`,
|
||||||
|
maxCapacity: scene.MaxPlayers,
|
||||||
|
isFull: false,
|
||||||
|
isPrivate: typeof options.Private !== 'boolean' ? false : options.Private,
|
||||||
|
isInProgress: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getAllRoomInstances(options.Room.Room.RoomId).add(newInstance);
|
||||||
|
if (options.FirstPlayer) {
|
||||||
|
this.setPlayerInstance(options.FirstPlayer, newInstance);
|
||||||
|
this.getInstancePlayers(newInstance).add(options.FirstPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call only when the player is ready to be moved to an instance
|
||||||
|
*
|
||||||
|
* Synchronizes profile instance to `instance` and adds player to instance.
|
||||||
|
*/
|
||||||
setPlayerInstance(player: Profile, instance: RoomInstance) {
|
setPlayerInstance(player: Profile, instance: RoomInstance) {
|
||||||
const currentInstance = player.getInstance();
|
const currentInstance = player.getInstance();
|
||||||
if (currentInstance === instance) return;
|
if (currentInstance === instance) return;
|
||||||
@@ -128,6 +149,19 @@ class InstancesBase {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call only when the player is ready to be removed (or when not responding)
|
||||||
|
*
|
||||||
|
* Synchronizes profile instance to `null` and removes player from instance.
|
||||||
|
*/
|
||||||
|
removePlayerFromCurrentInstance(player: Profile) {
|
||||||
|
const instance = player.getInstance();
|
||||||
|
if (!instance) return;
|
||||||
|
this.getInstancePlayers(instance).delete(player);
|
||||||
|
player.setInstance(null);
|
||||||
|
this.updateSingleInstanceIsFull(instance);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Instances = new InstancesBase();
|
const Instances = new InstancesBase();
|
||||||
|
|||||||
@@ -1,10 +1,160 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { SettingKey } from "../content/settings.ts";
|
||||||
|
import Profile from "../profiles.ts";
|
||||||
|
import { DeviceClass, PlayerStatusVisibility, RoomInstance, VRMovementMode } from "./types.ts";
|
||||||
|
import Logging from "@proxnet/undead-logging";
|
||||||
|
|
||||||
|
const log = new Logging("Presence");
|
||||||
|
|
||||||
|
interface PresenceExport {
|
||||||
|
roomInstance: RoomInstance | null;
|
||||||
|
playerId: number;
|
||||||
|
statusVisibility: PlayerStatusVisibility;
|
||||||
|
deviceClass: DeviceClass;
|
||||||
|
vrMovementMode?: VRMovementMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hot mess
|
||||||
|
class PlayerPresence {
|
||||||
|
|
||||||
|
intervalId: number;
|
||||||
|
|
||||||
|
#profile: Profile;
|
||||||
|
|
||||||
|
constructor(player: Profile, offline: boolean | undefined = false) {
|
||||||
|
|
||||||
|
this.offline = offline;
|
||||||
|
this.#profile = player;
|
||||||
|
this.playerId = player.getId();
|
||||||
|
this.roomInstance = this.#profile.getInstance();
|
||||||
|
|
||||||
|
if (offline) this.statusVisibility = PlayerStatusVisibility.Offline;
|
||||||
|
else this.statusVisibility = PlayerStatusVisibility.FriendsOnly;
|
||||||
|
this.deviceClass = DeviceClass.Unknown;
|
||||||
|
this.vrMovementMode = VRMovementMode.Teleport;
|
||||||
|
|
||||||
|
this.lastSeen = new Date();
|
||||||
|
|
||||||
|
this.intervalId = setInterval(() => {
|
||||||
|
this.updateOffline();
|
||||||
|
}, 80000);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
offline: boolean;
|
||||||
|
|
||||||
|
playerId: number;
|
||||||
|
statusVisibility: PlayerStatusVisibility;
|
||||||
|
deviceClass: DeviceClass;
|
||||||
|
vrMovementMode: VRMovementMode | undefined;
|
||||||
|
roomInstance: RoomInstance | null;
|
||||||
|
|
||||||
|
lastSeen: Date;
|
||||||
|
|
||||||
|
async updateStatusVisibility() {
|
||||||
|
const PlayerStatusVisibilityEnum = z.nativeEnum(PlayerStatusVisibility);
|
||||||
|
type PlayerStatusVisibilityEnum = z.infer<typeof PlayerStatusVisibilityEnum>;
|
||||||
|
|
||||||
|
const visibilityResult = PlayerStatusVisibilityEnum.safeParse(await this.#profile.getSetting(SettingKey.PlayerStatusVisibility));
|
||||||
|
if (visibilityResult.success) this.statusVisibility = visibilityResult.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEnums() {
|
||||||
|
if (!this.offline) await this.updateStatusVisibility();
|
||||||
|
else this.statusVisibility = PlayerStatusVisibility.Offline;
|
||||||
|
|
||||||
|
// deviceClass
|
||||||
|
const DeviceClassEnum = z.nativeEnum(DeviceClass);
|
||||||
|
type DeviceClassEnum = z.infer<typeof DeviceClassEnum>;
|
||||||
|
|
||||||
|
const deviceClassResult = DeviceClassEnum.safeParse(await this.#profile.getKnownDeviceClass());
|
||||||
|
if (deviceClassResult.success) this.deviceClass = deviceClassResult.data;
|
||||||
|
|
||||||
|
// vrMovementMode
|
||||||
|
const VRMovementModeEnum = z.nativeEnum(VRMovementMode);
|
||||||
|
type VRMovementModeEnum = z.infer<typeof VRMovementModeEnum>;
|
||||||
|
|
||||||
|
const vrMovementMoveResult = VRMovementModeEnum.safeParse(await this.#profile.getVRMovementMode());
|
||||||
|
if (vrMovementMoveResult.success) this.vrMovementMode = vrMovementMoveResult.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async export() {
|
||||||
|
await this.updateEnums();
|
||||||
|
const exp: PresenceExport = {
|
||||||
|
playerId: this.playerId,
|
||||||
|
roomInstance: this.roomInstance,
|
||||||
|
statusVisibility: this.statusVisibility,
|
||||||
|
deviceClass: this.deviceClass,
|
||||||
|
vrMovementMode: this.vrMovementMode ? this.vrMovementMode : undefined
|
||||||
|
}
|
||||||
|
return Object.assign({}, exp); // hard copy/clone
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOffline() {
|
||||||
|
if (Math.round(new Date().getTime() / 1000) - Math.round(this.lastSeen.getTime() / 1000) >= 60) this.offline = true;
|
||||||
|
else this.offline = false;
|
||||||
|
|
||||||
|
this.lastSeen = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const presence: Set<PlayerPresence> = new Set();
|
||||||
|
|
||||||
class PresenceBase {
|
class PresenceBase {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Heavy. Use with caution.
|
||||||
|
*/
|
||||||
|
async getAllPresences() {
|
||||||
|
const presSet: Set<PresenceExport> = new Set();
|
||||||
|
for (const pres of presence.values()) presSet.add(await pres.export());
|
||||||
|
return presSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllRawPresences() {
|
||||||
|
return presence;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(player: Profile) {
|
||||||
|
if (!presence.values().find(pres => pres.playerId == player.getId())) {
|
||||||
|
const pres = new PlayerPresence(player);
|
||||||
|
await pres.updateEnums();
|
||||||
|
presence.add(pres);
|
||||||
|
}
|
||||||
|
log.d(`Presences: ${JSON.stringify(Array.from(await Presence.getAllPresences()))}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(player: Profile) {
|
||||||
|
const pres = presence.values().find(pres => pres.playerId == player.getId());
|
||||||
|
if (pres) return pres;
|
||||||
|
else {
|
||||||
|
const pres = new PlayerPresence(player, true);
|
||||||
|
await pres.updateEnums();
|
||||||
|
presence.add(pres);
|
||||||
|
return pres;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Presence = new PresenceBase();
|
const Presence = new PresenceBase();
|
||||||
|
|
||||||
|
const id = setInterval(() => {
|
||||||
|
Presence.deleteDeadPresences();
|
||||||
|
}, 480000); // delete dead presences every 8 minutes
|
||||||
|
Deno.addSignalListener("SIGINT", async () => {
|
||||||
|
clearInterval(id);
|
||||||
|
const presArray = Presence.getAllRawPresences();
|
||||||
|
for (const pres of presArray.values()) clearInterval(pres.intervalId);
|
||||||
|
});
|
||||||
|
|
||||||
export default Presence;
|
export default Presence;
|
||||||
91
src/data/live/types.ts
Normal file
91
src/data/live/types.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { IntegratedRoomScene, RoomDetails } from "../content/roomtypes.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
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoomInstance {
|
||||||
|
|
||||||
|
roomInstanceId: number,
|
||||||
|
roomId: number,
|
||||||
|
subRoomId: number,
|
||||||
|
location: IntegratedRoomScene,
|
||||||
|
dataBlob?: string,
|
||||||
|
eventId?: number,
|
||||||
|
photonRegionId: PhotonRegionCodeString,
|
||||||
|
photonRoomId: string,
|
||||||
|
name?: string,
|
||||||
|
maxCapacity: number,
|
||||||
|
isFull: boolean,
|
||||||
|
isPrivate: boolean,
|
||||||
|
isInProgress: boolean
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceOptions {
|
||||||
|
|
||||||
|
Room: RoomDetails,
|
||||||
|
SceneIndex: number,
|
||||||
|
EventId?: number,
|
||||||
|
Name?: string,
|
||||||
|
Private?: boolean,
|
||||||
|
FirstPlayer?: Profile
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DeviceClass {
|
||||||
|
Unknown,
|
||||||
|
VR,
|
||||||
|
Screen,
|
||||||
|
Mobile,
|
||||||
|
VRLow
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PlayerStatusVisibility {
|
||||||
|
Public,
|
||||||
|
FriendsOnly,
|
||||||
|
FavoriteFriendsOnly,
|
||||||
|
Offline
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum VRMovementMode {
|
||||||
|
Teleport,
|
||||||
|
Walk
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlayerPresence {
|
||||||
|
playerId: number;
|
||||||
|
statusVisibility: PlayerStatusVisibility;
|
||||||
|
deviceClass: DeviceClass;
|
||||||
|
vrMovementMode: VRMovementMode;
|
||||||
|
roomInstance: RoomInstance;
|
||||||
|
}
|
||||||
10
src/data/platformtypes.ts
Normal file
10
src/data/platformtypes.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export enum PlatformMask {
|
||||||
|
None = 0,
|
||||||
|
Steam = 1,
|
||||||
|
Oculus = 2,
|
||||||
|
PlayStation = 4,
|
||||||
|
Microsoft = 8,
|
||||||
|
HeadlessBot = 16,
|
||||||
|
IOS = 32,
|
||||||
|
All = -1
|
||||||
|
}
|
||||||
@@ -4,7 +4,11 @@ import { Config } from "../config.ts";
|
|||||||
import { AuthType } from "./users.ts";
|
import { AuthType } from "./users.ts";
|
||||||
import * as JsonWebToken from "@gz/jwt";
|
import * as JsonWebToken from "@gz/jwt";
|
||||||
import { TokenBaseFormat } from "../apiutils.ts";
|
import { TokenBaseFormat } from "../apiutils.ts";
|
||||||
import { RoomInstance } from "./live/instances.ts";
|
import { DeviceClass, RoomInstance, VRMovementMode } from "./live/types.ts";
|
||||||
|
import { Setting } from "./profiletypes.ts";
|
||||||
|
import { SettingKey } from "./content/settings.ts";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { SignalRSocketHandler } from "../socket/socket.ts";
|
||||||
|
|
||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|
||||||
@@ -98,73 +102,24 @@ class Profile {
|
|||||||
// surely this can be written better
|
// surely this can be written better
|
||||||
static getExportAccount(id: number): Promise<AccountExport | null> {
|
static getExportAccount(id: number): Promise<AccountExport | null> {
|
||||||
return new Promise((resolve, _reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
Redis.Database.get(
|
Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.Username)).then((val) => {
|
||||||
Redis.buildKey(
|
|
||||||
Redis.KeyGroups.Profiles.Root,
|
|
||||||
id.toString(),
|
|
||||||
Redis.KeyGroups.Profiles.Username,
|
|
||||||
),
|
|
||||||
).then((val) => {
|
|
||||||
if (val == null) resolve(null);
|
if (val == null) resolve(null);
|
||||||
else {
|
else {
|
||||||
const promises = {
|
const promises = {
|
||||||
profileImage: Redis.Database.get(
|
profileImage: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.ProfileImage)),
|
||||||
Redis.buildKey(
|
isJunior: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.Junior)),
|
||||||
Redis.KeyGroups.Profiles.Root,
|
username: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.Username)),
|
||||||
id.toString(),
|
displayName: Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, id.toString(), Redis.KeyGroups.Profiles.DisplayName)),
|
||||||
Redis.KeyGroups.Profiles.ProfileImage,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
isJunior: Redis.Database.get(
|
|
||||||
Redis.buildKey(
|
|
||||||
Redis.KeyGroups.Profiles.Root,
|
|
||||||
id.toString(),
|
|
||||||
Redis.KeyGroups.Profiles.Junior,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
platforms: Redis.Database.get(
|
|
||||||
Redis.buildKey(
|
|
||||||
Redis.KeyGroups.Profiles.Root,
|
|
||||||
id.toString(),
|
|
||||||
Redis.KeyGroups.Profiles.Platforms,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
displayName: Redis.Database.get(
|
|
||||||
Redis.buildKey(
|
|
||||||
Redis.KeyGroups.Profiles.Root,
|
|
||||||
id.toString(),
|
|
||||||
Redis.KeyGroups.Profiles.DisplayName,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
username: Redis.Database.get(
|
|
||||||
Redis.buildKey(
|
|
||||||
Redis.KeyGroups.Profiles.Root,
|
|
||||||
id.toString(),
|
|
||||||
Redis.KeyGroups.Profiles.Username,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Promise.all(Object.values(promises)).then((values) => {
|
Promise.all(Object.values(promises)).then((values) => {
|
||||||
resolve({
|
resolve({
|
||||||
accountId: id,
|
accountId: id,
|
||||||
profileImage: values[0] == null
|
profileImage: values[0] == null ? "DefaultProfileImage.png" : values[0],
|
||||||
? "DefaultProfileImage.png"
|
isJunior: values[1] == null ? false : JSON.parse(values[1]),
|
||||||
: values[0],
|
platforms: 1,
|
||||||
isJunior: values[1] == null
|
username: values[2] == null ? "DATABASEERROR" : values[2],
|
||||||
? false
|
displayName: values[3] == null ? (values[2] == null ? "DATABASEERROR" : values[2]) : values[3],
|
||||||
: JSON.parse(values[1]),
|
|
||||||
platforms: values[2] == null
|
|
||||||
? 1
|
|
||||||
: JSON.parse(values[2]),
|
|
||||||
displayName: values[3] == null
|
|
||||||
? (values[4] == null
|
|
||||||
? "DATABASEERROR"
|
|
||||||
: values[4])
|
|
||||||
: values[3],
|
|
||||||
username: values[4] == null
|
|
||||||
? "DATABASEERROR"
|
|
||||||
: values[4],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -183,11 +138,13 @@ class Profile {
|
|||||||
|
|
||||||
#instance: RoomInstance | null = null;
|
#instance: RoomInstance | null = null;
|
||||||
|
|
||||||
|
#socket: SignalRSocketHandler | null = null;
|
||||||
|
|
||||||
constructor(id: number) {
|
constructor(id: number) {
|
||||||
this.#id = id;
|
this.#id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
setInstance(instance: RoomInstance) {
|
setInstance(instance: RoomInstance | null) {
|
||||||
this.#instance = instance;
|
this.#instance = instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,18 +164,81 @@ class Profile {
|
|||||||
return await Profile.getExportAccount(this.#id);
|
return await Profile.getExportAccount(this.#id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getSettings() {
|
||||||
|
const settings = await Redis.Database.hgetall(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.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.#id.toString(), Redis.KeyGroups.Profiles.Settings), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSetting(key: SettingKey, value: string) {
|
||||||
|
await Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Settings), key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delSetting(key: SettingKey) {
|
||||||
|
await Redis.Database.hdel(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Settings), key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delAllSettings() {
|
||||||
|
await Redis.Database.del(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
async setKnownDeviceClass(deviceClass: string | number) {
|
||||||
|
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DeviceClass), deviceClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getKnownDeviceClass() {
|
||||||
|
const data = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DeviceClass));
|
||||||
|
|
||||||
|
const DeviceClassEnum = z.nativeEnum(DeviceClass);
|
||||||
|
type DeviceClassEnum = z.infer<typeof DeviceClassEnum>
|
||||||
|
|
||||||
|
const result = DeviceClassEnum.safeParse(data);
|
||||||
|
if (result.success) return result.data;
|
||||||
|
else return DeviceClass.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setVRMovementMode(movementMode: string | number) {
|
||||||
|
return await this.setSetting(SettingKey.VRMovementMode, movementMode.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVRMovementMode() {
|
||||||
|
const data = await this.getSetting(SettingKey.VRMovementMode);
|
||||||
|
|
||||||
|
const VRMovementModeEnum = z.nativeEnum(VRMovementMode);
|
||||||
|
type VRMovementModeEnum = z.infer<typeof VRMovementModeEnum>
|
||||||
|
|
||||||
|
const result = VRMovementModeEnum.safeParse(data);
|
||||||
|
if (result.success) return result.data;
|
||||||
|
else return VRMovementMode.Teleport;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSocketHandler(handler: SignalRSocketHandler) {
|
||||||
|
this.#socket = handler;
|
||||||
|
}
|
||||||
|
clearSocketHandler() {
|
||||||
|
this.#socket = null;
|
||||||
|
}
|
||||||
|
getSocketHandler() {
|
||||||
|
return this.#socket;
|
||||||
|
}
|
||||||
|
|
||||||
// get, set instance
|
// get, set instance
|
||||||
// this.#instance: RoomInstance
|
// this.#instance: RoomInstance
|
||||||
|
|
||||||
async getToken() {
|
async getToken() {
|
||||||
const payload: ProfileTokenFormat = {
|
const payload: ProfileTokenFormat = {
|
||||||
iss: config.web.publichost,
|
iss: `${config.web.securepublichost ? 'https' : 'http'}://${config.web.publichost}`,
|
||||||
sub: this.#id,
|
sub: this.#id,
|
||||||
role: (await this.getIsOperator()) ? 'developer' : 'user',
|
role: (await this.getIsOperator()) ? 'developer' : 'user',
|
||||||
exp: Math.round(Date.now() / 1000) + Math.round(config.auth.timeout * 60 * 60),
|
exp: Math.round(Date.now() / 1000) + Math.round(config.auth.timeout * 60 * 60),
|
||||||
typ: AuthType.Game
|
typ: AuthType.Game
|
||||||
};
|
};
|
||||||
return await JsonWebToken.encode(payload, config.auth.secret, {algorithm: "HS512"});
|
return await JsonWebToken.encode(payload, config.auth.secret, { algorithm: "HS512" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
src/data/profiletypes.ts
Normal file
4
src/data/profiletypes.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface Setting {
|
||||||
|
Key: string;
|
||||||
|
Value: string;
|
||||||
|
}
|
||||||
@@ -78,7 +78,7 @@ export class User {
|
|||||||
|
|
||||||
async getToken() {
|
async getToken() {
|
||||||
const payload: UserTokenFormat = {
|
const payload: UserTokenFormat = {
|
||||||
iss: config.web.publichost,
|
iss: `${config.web.securepublichost ? 'https' : 'http'}://${config.web.publichost}`,
|
||||||
sub: this.#client_id,
|
sub: this.#client_id,
|
||||||
exp: Math.round(Date.now() / 1000) + (config.auth.timeout * 60 * 60),
|
exp: Math.round(Date.now() / 1000) + (config.auth.timeout * 60 * 60),
|
||||||
typ: AuthType.Web
|
typ: AuthType.Web
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ export const KeyGroups = {
|
|||||||
ProfileImage: "profileImage",
|
ProfileImage: "profileImage",
|
||||||
Junior: "isJunior",
|
Junior: "isJunior",
|
||||||
Platforms: "platforms",
|
Platforms: "platforms",
|
||||||
DisplayName: "displayname",
|
DisplayName: "displayName",
|
||||||
|
Settings: "settings",
|
||||||
|
DeviceClass: "deviceClass",
|
||||||
},
|
},
|
||||||
Operators: "operators",
|
Operators: "operators",
|
||||||
Users: {
|
Users: {
|
||||||
|
|||||||
85
src/main.ts
85
src/main.ts
@@ -1,11 +1,19 @@
|
|||||||
import * as Log from "@proxnet/undead-logging";
|
import * as Log from "@proxnet/undead-logging";
|
||||||
import * as Config from "./config.ts";
|
import * as Config from "./config.ts";
|
||||||
import { Database } from "./db.ts";
|
import { Database } from "./db.ts";
|
||||||
import { APIUtils } from "./apiutils.ts";
|
import { APIUtils, ProfileTokenSchema } from "./apiutils.ts";
|
||||||
import { Discord } from "./discord.ts";
|
import { Discord } from "./discord.ts";
|
||||||
import { generateRandomString } from "./apiutils.ts";
|
import { generateRandomString } from "./apiutils.ts";
|
||||||
// @ts-types = "npm:@types/express"
|
// @ts-types = "npm:@types/express"
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
import WebSocket, { WebSocketServer } from "ws";
|
||||||
|
import { IncomingMessage } from "../../AppData/Local/deno/npm/registry.npmjs.org/@types/connect/3.4.38/index.d.ts";
|
||||||
|
import { decode } from "@gz/jwt";
|
||||||
|
import Profile, { ProfileTokenFormat } from "./data/profiles.ts";
|
||||||
|
import { SocketHandoff } from "./socket/handoff.ts";
|
||||||
|
import internal from "node:stream";
|
||||||
|
import { Buffer } from "node:buffer";
|
||||||
|
import { SignalRSocketHandler } from "./socket/socket.ts";
|
||||||
|
|
||||||
const instanceId = generateRandomString(64);
|
const instanceId = generateRandomString(64);
|
||||||
|
|
||||||
@@ -20,15 +28,11 @@ if (typeof config == "undefined") {
|
|||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
}
|
}
|
||||||
if (config.auth.secret == Config.defaultConfig.auth.secret) {
|
if (config.auth.secret == Config.defaultConfig.auth.secret) {
|
||||||
log.e(
|
log.e(`Cannot start: Auth secret is default. Please change 'secrets.authSecret' in 'config.json'`);
|
||||||
`Cannot start: Auth secret is default. Please change 'secrets.authSecret' in 'config.json'`,
|
|
||||||
);
|
|
||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
}
|
}
|
||||||
if (config.public.serverId == Config.defaultConfig.public.serverId) {
|
if (config.public.serverId == Config.defaultConfig.public.serverId) {
|
||||||
log.e(
|
log.e(`Cannot start: Server ID is default. Please change 'public.serverId' in 'config.json'`);
|
||||||
`Cannot start: Server ID is default. Please change 'public.serverId' in 'config.json'`,
|
|
||||||
);
|
|
||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +81,7 @@ const authRouter = await import("./routes/auth.ts");
|
|||||||
const accountRouter = await import("./routes/account.ts");
|
const accountRouter = await import("./routes/account.ts");
|
||||||
const imgRouter = await import("./routes/img.ts");
|
const imgRouter = await import("./routes/img.ts");
|
||||||
const matchRouter = await import("./routes/match.ts");
|
const matchRouter = await import("./routes/match.ts");
|
||||||
|
const notifyRouter = await import("./socket/route.ts");
|
||||||
|
|
||||||
app.use(nameserverRouter.route.path, nameserverRouter.route.router);
|
app.use(nameserverRouter.route.path, nameserverRouter.route.router);
|
||||||
app.use(apiRouter.route.path, apiRouter.route.router);
|
app.use(apiRouter.route.path, apiRouter.route.router);
|
||||||
@@ -85,21 +90,16 @@ app.use(authRouter.route.path, authRouter.route.router);
|
|||||||
app.use(accountRouter.route.path, accountRouter.route.router);
|
app.use(accountRouter.route.path, accountRouter.route.router);
|
||||||
app.use(imgRouter.route.path, imgRouter.route.router);
|
app.use(imgRouter.route.path, imgRouter.route.router);
|
||||||
app.use(matchRouter.route.path, matchRouter.route.router);
|
app.use(matchRouter.route.path, matchRouter.route.router);
|
||||||
|
app.use(notifyRouter.route.path, notifyRouter.route.router);
|
||||||
|
|
||||||
app.use((rq: express.Request, rs: express.Response) => {
|
app.use((rq: express.Request, rs: express.Response) => {
|
||||||
log.e(
|
log.e(`${APIUtils.getSrcIpDefault(rq)} 404 ${rq.method} ${rq.url.toString()}`);
|
||||||
`${APIUtils.getSrcIpDefault(rq)} 404 ${rq.method} ${rq.url.toString()}`,
|
|
||||||
);
|
|
||||||
rs.statusCode = 404;
|
rs.statusCode = 404;
|
||||||
rs.json(
|
rs.json(APIUtils.genericResponseFormat(true, "Endpoint not found. Check your syntax and/or method."));
|
||||||
APIUtils.genericResponseFormat(
|
|
||||||
true,
|
|
||||||
"Endpoint not found. Check your syntax and/or method.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const http = app.listen(config.web.port, config.web.host, () => {
|
const http = app.listen(config.web.port, config.web.host, () => {
|
||||||
log.n(`Listening on http://${config.web.host}:${config.web.port}`);
|
log.n(`Listening on http://${config.web.host}:${config.web.port}`);
|
||||||
|
|
||||||
@@ -112,6 +112,59 @@ try {
|
|||||||
http.close();
|
http.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const wss = new WebSocketServer({
|
||||||
|
server: http,
|
||||||
|
path: "/notify/hub/v1"
|
||||||
|
});
|
||||||
|
wss.on('connection', (ws: WebSocket, rq: IncomingMessage, profile: Profile, connectionId: string) => {
|
||||||
|
const handoff = SocketHandoff.find(connectionId);
|
||||||
|
if (handoff) handoff.complete();
|
||||||
|
log.d(typeof profile);
|
||||||
|
new SignalRSocketHandler(ws, profile);
|
||||||
|
});
|
||||||
|
http.on('upgrade', async (rq: IncomingMessage, socket: internal.Duplex, head: Buffer) => {
|
||||||
|
const errorHandler = (err: Error | undefined) => { log.e(`Socket error: ${err?.stack}`); };
|
||||||
|
socket.on('error', errorHandler);
|
||||||
|
|
||||||
|
function writeUnauthorized() {
|
||||||
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
||||||
|
socket.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
let wrong = false;
|
||||||
|
const unparsedToken = rq.headers['Authorization'];
|
||||||
|
const connectionId = new URL(`http://${rq.headers.host ? rq.headers.host : 'localhost'}${rq.url}`).searchParams.get('connectionId');
|
||||||
|
if (connectionId == null) wrong = true;
|
||||||
|
else {
|
||||||
|
if (typeof unparsedToken == 'string') {
|
||||||
|
const splitToken = unparsedToken.split(' ')[1]
|
||||||
|
if (splitToken) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const decodedToken = await decode<ProfileTokenFormat>(splitToken, config.auth.secret, {algorithm: 'HS512'});
|
||||||
|
const schemaResult = ProfileTokenSchema.safeParse(decodedToken);
|
||||||
|
if (!schemaResult.success) wrong = true;
|
||||||
|
else {
|
||||||
|
wss.handleUpgrade(rq, socket, head, (ws) => {
|
||||||
|
wss.emit('connection', ws, rq, new Profile(decodedToken.sub), connectionId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
wrong = true;
|
||||||
|
}
|
||||||
|
} else wrong = true;
|
||||||
|
} else wrong = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wrong) {
|
||||||
|
writeUnauthorized();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socket.removeListener('error', errorHandler);
|
||||||
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.e(`Cannot start: Network could not be initalized. ${err}`);
|
log.e(`Cannot start: Network could not be initalized. ${err}`);
|
||||||
Deno.exit(1);
|
Deno.exit(1);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { route as VersionCheckRoute } from "./api/versioncheck.ts";
|
import { route as VersionCheckRoute } from "./api/versioncheck.ts";
|
||||||
import { route as ConfigRoute } from "./api/config.ts";
|
import { route as ConfigRoute } from "./api/config.ts";
|
||||||
import { route as GameConfig } from "./api/gameconfigs.ts";
|
import { route as GameConfig } from "./api/gameconfigs.ts";
|
||||||
|
import { route as PlayerReportingRoute } from "./api/PlayerReporting.ts";
|
||||||
import { APIUtils } from "../apiutils.ts";
|
import { APIUtils } from "../apiutils.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter("/api");
|
export const route = APIUtils.createRouter("/api");
|
||||||
@@ -8,3 +9,4 @@ export const route = APIUtils.createRouter("/api");
|
|||||||
route.router.use(VersionCheckRoute.path, VersionCheckRoute.router);
|
route.router.use(VersionCheckRoute.path, VersionCheckRoute.router);
|
||||||
route.router.use(ConfigRoute.path, ConfigRoute.router);
|
route.router.use(ConfigRoute.path, ConfigRoute.router);
|
||||||
route.router.use(GameConfig.path, GameConfig.router);
|
route.router.use(GameConfig.path, GameConfig.router);
|
||||||
|
route.router.use(PlayerReportingRoute.path, PlayerReportingRoute.router);
|
||||||
@@ -27,3 +27,19 @@ route.router.post('/v1/hile',
|
|||||||
},
|
},
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
route.router.get('/v1/moderationBlockDetails',
|
||||||
|
|
||||||
|
APIUtils.Authentication,
|
||||||
|
|
||||||
|
(_rq, rs) => {
|
||||||
|
// todo: moderation
|
||||||
|
rs.json({
|
||||||
|
ReportCategory: 0,
|
||||||
|
Duration: 0,
|
||||||
|
GameSessionId: 0,
|
||||||
|
Message: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
@@ -76,7 +76,6 @@ route.router.post("/token",
|
|||||||
|
|
||||||
APIUtils.Authentication,
|
APIUtils.Authentication,
|
||||||
express.urlencoded({ extended: true }),
|
express.urlencoded({ extended: true }),
|
||||||
APIUtils.logBody,
|
|
||||||
APIUtils.validateRequestBody<AuthBodyBase>(TokenRequestBodySchema),
|
APIUtils.validateRequestBody<AuthBodyBase>(TokenRequestBodySchema),
|
||||||
|
|
||||||
async (
|
async (
|
||||||
@@ -93,9 +92,10 @@ route.router.post("/token",
|
|||||||
}
|
}
|
||||||
|
|
||||||
const conditionsMet = ![
|
const conditionsMet = ![
|
||||||
rq.body.client_id == "recroom",
|
rq.body.client_id === "recroom",
|
||||||
rq.body.platform == "0",
|
rq.body.platform === "0",
|
||||||
rq.body.ver == '20191120',
|
rq.body.ver === '20191120',
|
||||||
|
rq.body.device_class.length === 1,
|
||||||
!(rq.body.device_id.length > 96),
|
!(rq.body.device_id.length > 96),
|
||||||
!(rq.body.client_secret.length > 96),
|
!(rq.body.client_secret.length > 96),
|
||||||
!(rq.body.platform_id.length > 32),
|
!(rq.body.platform_id.length > 32),
|
||||||
@@ -107,34 +107,12 @@ route.router.post("/token",
|
|||||||
requestFailed();
|
requestFailed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (rq.body.grant_type == 'cached_login') {
|
|
||||||
const accounts = await rs.locals.user.getAssociatedProfiles();
|
|
||||||
const targetAccount = parseInt(rq.body.account_id);
|
|
||||||
|
|
||||||
if (isNaN(targetAccount)) {
|
const accounts = await rs.locals.user.getAssociatedProfiles();
|
||||||
requestFailed();
|
let targetAccount: number;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!accounts.has(targetAccount)) {
|
|
||||||
requestFailed("access_denied");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
rs.locals.user.addAssociatedDeviceId(rq.body.device_id);
|
if (rq.body.grant_type == 'cached_login') targetAccount = parseInt(rq.body.account_id);
|
||||||
rs.locals.user.addAssociatedPlatformId(rq.body.platform_id);
|
else {
|
||||||
|
|
||||||
const profile = new Profile(targetAccount);
|
|
||||||
if (!(await Profile.exists(profile.getId()))) {
|
|
||||||
requestFailed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await profile.getToken();
|
|
||||||
rs.json({
|
|
||||||
access_token: token,
|
|
||||||
refresh_token: token,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const refreshToken = rq.body.refresh_token;
|
const refreshToken = rq.body.refresh_token;
|
||||||
if (typeof refreshToken == 'undefined') {
|
if (typeof refreshToken == 'undefined') {
|
||||||
requestFailed();
|
requestFailed();
|
||||||
@@ -149,32 +127,34 @@ route.router.post("/token",
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const accounts = await rs.locals.user.getAssociatedProfiles();
|
targetAccount = parseInt(decodedToken.sub ? decodedToken.sub : "NaN");
|
||||||
const targetAccount = parseInt(decodedToken.sub ? decodedToken.sub : "NaN");
|
|
||||||
|
|
||||||
if (isNaN(targetAccount)) {
|
|
||||||
requestFailed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!accounts.has(targetAccount)) {
|
|
||||||
requestFailed("access_denied");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
rs.locals.user.addAssociatedDeviceId(rq.body.device_id);
|
|
||||||
rs.locals.user.addAssociatedPlatformId(rq.body.platform_id);
|
|
||||||
|
|
||||||
const profile = new Profile(targetAccount);
|
|
||||||
if (!(await Profile.exists(profile.getId()))) {
|
|
||||||
requestFailed();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await profile.getToken();
|
|
||||||
rs.json({
|
|
||||||
access_token: token,
|
|
||||||
refresh_token: token,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isNaN(targetAccount)) {
|
||||||
|
requestFailed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!accounts.has(targetAccount)) {
|
||||||
|
requestFailed("access_denied");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rs.locals.user.addAssociatedDeviceId(rq.body.device_id);
|
||||||
|
rs.locals.user.addAssociatedPlatformId(rq.body.platform_id);
|
||||||
|
|
||||||
|
const profile = new Profile(targetAccount);
|
||||||
|
if (!(await Profile.exists(profile.getId()))) {
|
||||||
|
requestFailed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await profile.getToken();
|
||||||
|
rs.json({
|
||||||
|
access_token: token,
|
||||||
|
refresh_token: token,
|
||||||
|
});
|
||||||
|
|
||||||
|
await profile.setKnownDeviceClass(rq.body.device_class);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { APIUtils } from "../../apiutils.ts";
|
import { APIUtils, NoBody } from "../../apiutils.ts";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
|
import Matchmaking from "../../data/live/base.ts";
|
||||||
|
import Presence from "../../data/live/presence.ts";
|
||||||
|
|
||||||
export const route = APIUtils.createRouter('/player');
|
export const route = APIUtils.createRouter('/player');
|
||||||
|
|
||||||
@@ -18,13 +20,10 @@ route.router.post('/login',
|
|||||||
express.urlencoded({extended: true}),
|
express.urlencoded({extended: true}),
|
||||||
APIUtils.validateRequestBody(LoginSchema),
|
APIUtils.validateRequestBody(LoginSchema),
|
||||||
|
|
||||||
(_rq, rs) => {
|
(rq: express.Request<NoBody, NoBody, BaseLoginLock>, rs: express.Response) => {
|
||||||
// temporary
|
Matchmaking.createLoginLock(rs.locals.profile, rq.body.LoginLock);
|
||||||
|
Presence.create(rs.locals.profile);
|
||||||
rs.sendStatus(200);
|
rs.sendStatus(200);
|
||||||
// check for existing login
|
|
||||||
// set login lock
|
|
||||||
// init presence
|
|
||||||
// use device_class from token request
|
|
||||||
},
|
},
|
||||||
|
|
||||||
)
|
)
|
||||||
50
src/socket/handoff.ts
Normal file
50
src/socket/handoff.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
const handoffs: Set<SocketHandoff> = new Set();
|
||||||
|
|
||||||
|
function randomId(length: number) {
|
||||||
|
let result = '';
|
||||||
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
let counter = 0;
|
||||||
|
while (counter < length) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||||
|
counter += 1;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lots of this is redundant. The WebSocket request already contains an access token for the profile, but I'd
|
||||||
|
// like to make sure that connectionIds are freed automatically.
|
||||||
|
export class SocketHandoff {
|
||||||
|
|
||||||
|
static generateId() {
|
||||||
|
let id = randomId(48);
|
||||||
|
while (handoffs.values().find(handoff => handoff.id == id)) id = randomId(48);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
static find(id: string) {
|
||||||
|
return handoffs.values().find(handoff => handoff.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
#timeout: number;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.id = SocketHandoff.generateId();
|
||||||
|
|
||||||
|
this.#timeout = setTimeout(() => {
|
||||||
|
handoffs.delete(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
handoffs.add(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete() {
|
||||||
|
clearTimeout(this.#timeout);
|
||||||
|
handoffs.delete(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
complete() {
|
||||||
|
this.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
18
src/socket/route.ts
Normal file
18
src/socket/route.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { APIUtils } from "../apiutils.ts";
|
||||||
|
import { SocketHandoff } from "./handoff.ts";
|
||||||
|
|
||||||
|
export const route = APIUtils.createRouter('/notify');
|
||||||
|
|
||||||
|
route.router.post('/hub/v1/negotiate',
|
||||||
|
|
||||||
|
APIUtils.Authentication,
|
||||||
|
|
||||||
|
(_rq, rs) => {
|
||||||
|
const handoff = new SocketHandoff();
|
||||||
|
rs.json({
|
||||||
|
connectionId: handoff.id,
|
||||||
|
availableTransports: [{transport:"WebSockets",transferFormats:["Text"]}]
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
);
|
||||||
32
src/socket/socket.ts
Normal file
32
src/socket/socket.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import WebSocket from "ws";
|
||||||
|
import Profile from "../data/profiles.ts";
|
||||||
|
import { IncomingMessage } from "node:http";
|
||||||
|
import Logging from "@proxnet/undead-logging";
|
||||||
|
|
||||||
|
export class SignalRSocketHandler {
|
||||||
|
|
||||||
|
log: Logging = new Logging("SignalMock-");
|
||||||
|
|
||||||
|
#socket: WebSocket;
|
||||||
|
#profile: Profile;
|
||||||
|
|
||||||
|
constructor(socket: WebSocket, player: Profile) {
|
||||||
|
|
||||||
|
this.#socket = socket;
|
||||||
|
this.#profile = player;
|
||||||
|
|
||||||
|
player.setSocketHandler(this);
|
||||||
|
this.log.source += player.getId().toString();
|
||||||
|
|
||||||
|
// log: we connected!!
|
||||||
|
|
||||||
|
Deno.addSignalListener('SIGINT', this.destroy);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
this.#socket.close();
|
||||||
|
Deno.removeSignalListener('SIGINT', this.destroy);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
22
src/socket/types.ts
Normal file
22
src/socket/types.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export enum MessageTypes {
|
||||||
|
CancelInvocation,
|
||||||
|
Close,
|
||||||
|
Completion,
|
||||||
|
Handshake,
|
||||||
|
Invocation,
|
||||||
|
Ping,
|
||||||
|
StreamInvocation,
|
||||||
|
StreamItem,
|
||||||
|
Ack
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SignalRMessage {
|
||||||
|
arguments: object[],
|
||||||
|
error?: string,
|
||||||
|
invocationId?: string,
|
||||||
|
item?: object,
|
||||||
|
nonblocking: boolean,
|
||||||
|
result?: object,
|
||||||
|
target: string,
|
||||||
|
type: MessageTypes
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user