certainly something happened here

This commit is contained in:
2025-06-09 00:28:52 -04:00
22 changed files with 623 additions and 76 deletions

View File

@@ -1,5 +1,4 @@
name: Galvanic Corrosion Cross-Compile name: Galvanic Corrosion Cross-Compile
on: on:
push: push:
branches: [master] branches: [master]

16
.gitignore vendored
View File

@@ -129,12 +129,14 @@ dist
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
# galvanic corrosion
/build/
config.json
/user/
.vscode .vscode
# development purposes # galvanic corrosion
/graph.svg /user/
/config.json
# galvanic corrosion build process
/ver.ts.bak
/build
# used to attach license to each file
/append.ts

View File

@@ -14,11 +14,13 @@
"@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.2.0", "@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.2.0",
"@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/multer": "npm:@types/multer@^1.4.12",
"@types/validator": "npm:@types/validator@^13.12.2", "@types/validator": "npm:@types/validator@^13.12.2",
"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",
"multer": "npm:multer@^1.4.5-lts.2",
"validator": "npm:validator@^13.12.0", "validator": "npm:validator@^13.12.0",
"zod": "npm:zod@^3.24.2" "zod": "npm:zod@^3.24.2"
}, },

108
deno.lock generated
View File

@@ -11,6 +11,7 @@
"npm:@types/cookie-parser@^1.4.8": "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.0.0",
"npm:@types/express@5": "5.0.0", "npm:@types/express@5": "5.0.0",
"npm:@types/multer@^1.4.12": "1.4.12",
"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:chalk@^5.3.0": "5.3.0", "npm:chalk@^5.3.0": "5.3.0",
@@ -18,6 +19,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:multer@^1.4.5-lts.2": "1.4.5-lts.2",
"npm:validator@^13.12.0": "13.12.0", "npm:validator@^13.12.0": "13.12.0",
"npm:zod@^3.24.2": "3.24.2" "npm:zod@^3.24.2": "3.24.2"
}, },
@@ -163,6 +165,12 @@
"@types/mime@1.3.5": { "@types/mime@1.3.5": {
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
}, },
"@types/multer@1.4.12": {
"integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==",
"dependencies": [
"@types/express"
]
},
"@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": [
@@ -209,6 +217,9 @@
"negotiator" "negotiator"
] ]
}, },
"append-field@1.0.0": {
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw=="
},
"array-flatten@1.1.1": { "array-flatten@1.1.1": {
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
}, },
@@ -229,6 +240,15 @@
"unpipe" "unpipe"
] ]
}, },
"buffer-from@1.1.2": {
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
},
"busboy@1.6.0": {
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": [
"streamsearch"
]
},
"bytes@3.1.2": { "bytes@3.1.2": {
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
}, },
@@ -252,10 +272,19 @@
"cluster-key-slot@1.1.2": { "cluster-key-slot@1.1.2": {
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==" "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="
}, },
"concat-stream@1.6.2": {
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"dependencies": [
"buffer-from",
"inherits",
"readable-stream",
"typedarray"
]
},
"content-disposition@0.5.4": { "content-disposition@0.5.4": {
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dependencies": [ "dependencies": [
"safe-buffer" "safe-buffer@5.2.1"
] ]
}, },
"content-type@1.0.5": { "content-type@1.0.5": {
@@ -277,6 +306,9 @@
"cookie@0.7.2": { "cookie@0.7.2": {
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="
}, },
"core-util-is@1.0.3": {
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"debug@2.6.9": { "debug@2.6.9": {
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dependencies": [ "dependencies": [
@@ -385,7 +417,7 @@
"proxy-addr", "proxy-addr",
"qs", "qs",
"range-parser", "range-parser",
"safe-buffer", "safe-buffer@5.2.1",
"send", "send",
"serve-static", "serve-static",
"setprototypeof", "setprototypeof",
@@ -489,6 +521,9 @@
"ipaddr.js@1.9.1": { "ipaddr.js@1.9.1": {
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
}, },
"isarray@1.0.0": {
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"lodash.defaults@4.2.0": { "lodash.defaults@4.2.0": {
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
}, },
@@ -529,15 +564,40 @@
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"bin": true "bin": true
}, },
"minimist@1.2.8": {
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
},
"mkdirp@0.5.6": {
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": [
"minimist"
],
"bin": true
},
"ms@2.0.0": { "ms@2.0.0": {
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
}, },
"ms@2.1.3": { "ms@2.1.3": {
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}, },
"multer@1.4.5-lts.2": {
"integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==",
"dependencies": [
"append-field",
"busboy",
"concat-stream",
"mkdirp",
"object-assign",
"type-is",
"xtend"
]
},
"negotiator@0.6.3": { "negotiator@0.6.3": {
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
}, },
"object-assign@4.1.1": {
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
},
"object-inspect@1.13.3": { "object-inspect@1.13.3": {
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==" "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA=="
}, },
@@ -553,6 +613,9 @@
"path-to-regexp@0.1.12": { "path-to-regexp@0.1.12": {
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
}, },
"process-nextick-args@2.0.1": {
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"proxy-addr@2.0.7": { "proxy-addr@2.0.7": {
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"dependencies": [ "dependencies": [
@@ -578,6 +641,18 @@
"unpipe" "unpipe"
] ]
}, },
"readable-stream@2.3.8": {
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": [
"core-util-is",
"inherits",
"isarray",
"process-nextick-args",
"safe-buffer@5.1.2",
"string_decoder",
"util-deprecate"
]
},
"redis-errors@1.2.0": { "redis-errors@1.2.0": {
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==" "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="
}, },
@@ -587,6 +662,9 @@
"redis-errors" "redis-errors"
] ]
}, },
"safe-buffer@5.1.2": {
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safe-buffer@5.2.1": { "safe-buffer@5.2.1": {
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}, },
@@ -665,6 +743,15 @@
"statuses@2.0.1": { "statuses@2.0.1": {
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
}, },
"streamsearch@1.1.0": {
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="
},
"string_decoder@1.1.1": {
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": [
"safe-buffer@5.1.2"
]
},
"toidentifier@1.0.1": { "toidentifier@1.0.1": {
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
}, },
@@ -681,6 +768,9 @@
"mime-types" "mime-types"
] ]
}, },
"typedarray@0.0.6": {
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
},
"undici-types@6.19.8": { "undici-types@6.19.8": {
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
}, },
@@ -690,6 +780,9 @@
"unpipe@1.0.0": { "unpipe@1.0.0": {
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
}, },
"util-deprecate@1.0.2": {
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"utils-merge@1.0.1": { "utils-merge@1.0.1": {
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="
}, },
@@ -700,11 +793,10 @@
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
}, },
"ws@8.18.0": { "ws@8.18.0": {
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="
"optionalPeers": [ },
"bufferutil@^4.0.1", "xtend@4.0.2": {
"utf-8-validate@>=5.0.2" "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
]
}, },
"zod@3.24.2": { "zod@3.24.2": {
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==" "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="
@@ -857,11 +949,13 @@
"jsr:@proxnet/undead-logging@^1.2.0", "jsr:@proxnet/undead-logging@^1.2.0",
"npm:@types/cookie-parser@^1.4.8", "npm:@types/cookie-parser@^1.4.8",
"npm:@types/express@5", "npm:@types/express@5",
"npm:@types/multer@^1.4.12",
"npm:@types/validator@^13.12.2", "npm:@types/validator@^13.12.2",
"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:multer@^1.4.5-lts.2",
"npm:validator@^13.12.0", "npm:validator@^13.12.0",
"npm:zod@^3.24.2" "npm:zod@^3.24.2"
] ]

View File

@@ -21,9 +21,10 @@ import Logging from "@proxnet/undead-logging";
import { decode } from "@gz/jwt"; import { decode } from "@gz/jwt";
import { Config } from "./config.ts"; import { Config } from "./config.ts";
import { AuthType, User, UserTokenFormat } from "./data/users.ts"; import { AuthType, User, UserTokenFormat } from "./data/users.ts";
import UnifiedProfile, { ProfileTokenFormat } from "./data/profiles.ts"; import { ProfileTokenFormat } from "./data/profiles.ts";
import z from "zod"; import z from "zod";
import Matchmaking from "./data/live/base.ts"; import Matchmaking from "./data/live/base.ts";
import Server from "./data/server.ts";
const config = Config.getConfig(); const config = Config.getConfig();
@@ -101,7 +102,7 @@ export const validateQuery = <T>(schema: z.ZodSchema<T>) => (rq: express.Request
} }
}; };
type genericResponse = { export type genericResponse = {
failure: boolean, failure: boolean,
errors?: object, // zod only errors?: object, // zod only
message?: string, message?: string,
@@ -130,7 +131,7 @@ export function genericResponse(
rs.json({ failure: failure, errors: errors, message: msg, data: data }); rs.json({ failure: failure, errors: errors, message: msg, data: data });
}; };
} }
type RecNetResponse = { export type RecNetResponse = {
success: boolean; success: boolean;
error?: string; error?: string;
}; };
@@ -317,7 +318,13 @@ export async function Authentication(
].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);
else if (decodedToken.typ == AuthType.Game) rs.locals.profile = UnifiedProfile.get(decodedToken.sub); else if (decodedToken.typ == AuthType.Game) {
const profile = Server.UnifiedProfile.get(decodedToken.sub);
if (!profile) {
returnUnauthorized();
return;
} else rs.locals.profile = profile;
}
rs.locals.token = token; rs.locals.token = token;
nxt(); nxt();

146
src/data/content/cdn.ts Normal file
View File

@@ -0,0 +1,146 @@
/* Galvanic Corrosion - Rec Room custom server for communities.
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
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 <https://www.gnu.org/licenses/>. */
import Logging from "@proxnet/undead-logging";
import { generateRandomString } from "../../apiutils.ts";
import { Profile } from "../profiles.ts";
import * as fs from "node:fs";
import Server from "../server.ts";
const log = new Logging("CDN");
interface MetaFile {
creationPlayer?: Profile,
dateCreated: Date,
type: FileType
}
export interface File {
data: Uint8Array<ArrayBufferLike>,
meta: MetaFile
}
const userDataPath = './user';
export enum FileType {
Unknown,
RoomSave,
Holotar,
Image,
Video,
Invention
}
function fileTypeToString(type: FileType) {
switch (type) {
case FileType.Unknown:
return "data"
case FileType.RoomSave:
return "room"
case FileType.Holotar:
return "holo"
case FileType.Image:
return "img"
case FileType.Video:
return "vid"
case FileType.Invention:
return "inv"
}
}
export class CDNBase {
async #recurseDirCreate(filePath: string) {
const pathParts = `${userDataPath}/${filePath}`.split('/');
pathParts.pop();
const dirPath = pathParts.join('/');
log.d(dirPath);
if (dirPath) await Deno.mkdir(dirPath, { recursive: true });
}
async ensureUserDirectory() {
if (!fs.existsSync(userDataPath)) await Deno.mkdir(userDataPath);
}
async #generateFilename(type: FileType) {
let name = generateRandomString(24);
while (fs.existsSync(`${userDataPath}/${name}.${fileTypeToString(type)}`)) name = await this.#generateFilename(type);
if (type == FileType.RoomSave) return `room/${name}.${fileTypeToString(type)}`
else return `${name}.${fileTypeToString(type)}`;
}
/**
* Create a file, write to user directory
* @returns New filename
*/
async createFile(data: Uint8Array<ArrayBufferLike>, type: FileType, player: Profile) {
const filename = await this.#generateFilename(type);
await this.#recurseDirCreate(filename);
await Deno.writeFile(`${userDataPath}/${filename}`, data);
const meta = {
creationPlayer: player.getId(),
dateCreated: new Date().toISOString(),
type: type
}
await Deno.writeTextFile(`${userDataPath}/${filename}.gcmeta`, JSON.stringify(meta));
return filename;
}
async getFile(name: string) {
const path = `${userDataPath}/${name}`;
log.d(`Fetching CDN file: path '${path}'`);
try {
if (!(await Deno.stat(path)).isFile) return null;
else {
const data = await Deno.readFile(path);
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),
type: parsedMeta.type
}
const ret: File = {
data: data,
meta: meta
}
return ret;
}
} catch (err) {
log.w(`Could not fetch file: ${(err as Error).message}`);
return null;
}
}
async deleteFile(name: string) {
const path = `${userDataPath}/${name}`;
if (!(await Deno.stat(path)).isFile) return false;
else {
await Deno.remove(path);
await Deno.remove(`${path}.gcmeta`);
return true;
}
}
}

View File

@@ -22,6 +22,8 @@ import { BuiltinRoom, FactoryMode, IntegratedRoomScene, RoomAccessibility, RoomD
import { RoomFactory } from "./rooms/RoomFactory.ts"; import { RoomFactory } from "./rooms/RoomFactory.ts";
import { SubroomFactory } from "./rooms/SubroomFactory.ts"; import { SubroomFactory } from "./rooms/SubroomFactory.ts";
import { RootPath } from "../../path.ts"; import { RootPath } from "../../path.ts";
import { Instance } from "../live/instances.ts";
import { PushNotificationId } from "../../socket/types.ts";
const log = new Logging("Rooms"); const log = new Logging("Rooms");
@@ -299,6 +301,18 @@ class RoomsBase {
else return null; 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 Rooms = new RoomsBase();

View File

@@ -16,13 +16,14 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import Logging from "@proxnet/undead-logging"; import Logging from "@proxnet/undead-logging";
import UnifiedProfile, { Profile } from "../profiles.ts"; import { Profile } from "../profiles.ts";
import { RoomInstance, InstanceOptions } from "./types.ts"; import { RoomInstance, InstanceOptions } from "./types.ts";
import { Config } from "../../config.ts"; import { Config } from "../../config.ts";
import Presence from "./presence.ts"; import Presence from "./presence.ts";
import { RoomFactory } from "../content/rooms/RoomFactory.ts"; import { RoomFactory } from "../content/rooms/RoomFactory.ts";
import { RoomDataTypes } from "../content/rooms/DataTypes.ts"; import { RoomDataTypes } from "../content/rooms/DataTypes.ts";
import { PushNotificationId } from "../../socket/types.ts"; import { PushNotificationId } from "../../socket/types.ts";
import Server from "../server.ts";
const log = new Logging("Instances"); const log = new Logging("Instances");
@@ -55,13 +56,14 @@ export class Instance {
async init(options: InstanceOptions) { async init(options: InstanceOptions) {
const scene = options.Room.Scenes[options.SceneIndex]; const scene = options.Room.Scenes[options.SceneIndex];
if (!scene) throw new Error("The specified scene did not exist."); if (!scene) throw new Error("The specified scene does not exist.");
let instanceName; let instanceName;
if (scene.Name == 'Home' || scene.Name === options.Room.Room.Name) instanceName = `^${options.Room.Room.Name}`; if (scene.Name == 'Home' || scene.Name === options.Room.Room.Name) instanceName = `^${options.Room.Room.Name}`;
else instanceName = `^${options.Room.Room.Name}.${scene.Name}`; else instanceName = `^${options.Room.Room.Name}.${scene.Name}`;
if (options.IsDorm) { if (options.IsDorm) {
const dormCreatorPlayer = UnifiedProfile.get(options.Room.Room.CreatorPlayerId); 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(); const player = await dormCreatorPlayer.export();
if (player) instanceName = `@${player.displayName}'s Dorm`; if (player) instanceName = `@${player.displayName}'s Dorm`;
} }
@@ -95,6 +97,10 @@ export class Instance {
player.setInstance(null); player.setInstance(null);
} }
updatePlayers() {
for (const player of this.#players.values()) player.getSocketHandler()?.sendNotification(PushNotificationId.SubscriptionUpdateGameSession, this.snapshot());
}
async addPlayer(player: Profile) { async addPlayer(player: Profile) {
const currentInstance = player.getInstance(); const currentInstance = player.getInstance();
if (currentInstance && currentInstance.equalInstance(this)) return; if (currentInstance && currentInstance.equalInstance(this)) return;

View File

@@ -16,8 +16,9 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { Redis } from "../../db.ts"; import { Redis } from "../../db.ts";
import UnifiedProfile, { Profile } from "../profiles.ts"; import { Profile } from "../profiles.ts";
import { ProfileContentManager } from "./base/profilemanagerbase.ts"; import { ProfileContentManager } from "./base/profilemanagerbase.ts";
import Server from "../server.ts";
enum RelationshipType { enum RelationshipType {
None, None,
@@ -223,7 +224,11 @@ export class ProfileRelationshipManager extends ProfileContentManager {
async ignoreAllAssociatedPlatformUsers(platformid: string) { async ignoreAllAssociatedPlatformUsers(platformid: string) {
const ids = (await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, platformid))).map(val => parseInt(val)).filter(val => !isNaN(val)); 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) this.setPlayerIgnored(UnifiedProfile.get(id)); for (const id of ids) {
const profile = Server.UnifiedProfile.get(id);
if (!profile) continue;
this.setPlayerIgnored(profile);
}
} }
} }

View File

@@ -348,15 +348,18 @@ class Profile extends EventManager {
const profiles: Map<number, Profile> = new Map() const profiles: Map<number, Profile> = new Map()
// Control what is available to references export class UnifiedProfileBase {
class UnifiedProfileBase {
get(id: number) { get(id: number) {
let profile = profiles.get(id); let profile = profiles.get(id);
if (!profile) { if (!profile) {
try {
const inst = new Profile(id); const inst = new Profile(id);
profiles.set(id, inst); profiles.set(id, inst);
profile = inst; profile = inst;
} catch {
return null;
}
} }
return profile; return profile;
} }
@@ -384,7 +387,7 @@ class UnifiedProfileBase {
} }
const UnifiedProfile = new UnifiedProfileBase(); const UnifiedProfile = "";
export { Profile }; export { Profile };
export default UnifiedProfile; export default UnifiedProfile;

28
src/data/server.ts Normal file
View File

@@ -0,0 +1,28 @@
/* Galvanic Corrosion - Rec Room custom server for communities.
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
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 <https://www.gnu.org/licenses/>. */
import { EventManager } from "./baseevent.ts";
import { CDNBase } from "./content/cdn.ts";
import { UnifiedProfileBase } from "./profiles.ts";
class ServerBase extends EventManager {
CDN = new CDNBase();
UnifiedProfile = new UnifiedProfileBase();
}
const Server = new ServerBase();
export default Server;

View File

@@ -21,16 +21,16 @@ import { Database } from "./db.ts";
import { APIUtils, ProfileTokenSchema } 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"
import express from "express"; import express from "express";
import { decode } from "@gz/jwt"; import { decode } from "@gz/jwt";
import UnifiedProfile, { ProfileTokenFormat } from "./data/profiles.ts"; import { ProfileTokenFormat } from "./data/profiles.ts";
import { SocketHandoff } from "./socket/handoff.ts"; import { SocketHandoff } from "./socket/handoff.ts";
import { SignalRSocketHandler } from "./socket/socket.ts"; import { SignalRSocketHandler } from "./socket/socket.ts";
import { GameConfigs } from "./data/config.ts"; import { GameConfigs } from "./data/config.ts";
import { getVersion } from "./ver.ts"; import { getVersion } from "./ver.ts";
import Rooms from "./data/content/rooms.ts"; import Rooms from "./data/content/rooms.ts";
import { PushNotificationId } from "./socket/types.ts"; import { PushNotificationId } from "./socket/types.ts";
import Server from "./data/server.ts";
const instanceId = generateRandomString(64); const instanceId = generateRandomString(64);
@@ -71,6 +71,7 @@ app.disable("x-powered-by");
app.use( app.use(
(rq: express.Request, rs: express.Response, nxt: express.NextFunction) => { (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => {
rs.setHeader("Instance", instanceId); rs.setHeader("Instance", instanceId);
rs.setHeader('Access-Control-Allow-Origin', '*');
rs.locals.reqId = generateRandomString(12); rs.locals.reqId = generateRandomString(12);
log.n(`${rs.locals.reqId} ${APIUtils.getSrcIpDefault(rq)} ${rq.method} ${rq.originalUrl}`); log.n(`${rs.locals.reqId} ${APIUtils.getSrcIpDefault(rq)} ${rq.method} ${rq.originalUrl}`);
nxt(); nxt();
@@ -96,6 +97,7 @@ 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"); const notifyRouter = await import("./socket/route.ts");
const cdnRouter = await import("./routes/cdn.ts"); const cdnRouter = await import("./routes/cdn.ts");
const storageRouter = await import("./routes/storage.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);
@@ -106,6 +108,7 @@ 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(notifyRouter.route.path, notifyRouter.route.router);
app.use(cdnRouter.route.path, cdnRouter.route.router); app.use(cdnRouter.route.path, cdnRouter.route.router);
app.use(storageRouter.route.path, storageRouter.route.router);
// end content routes // end content routes
@@ -116,6 +119,7 @@ app.use((rq: express.Request, rs: express.Response) => {
}); });
if (!(await Rooms.generateBuiltinRooms())) log.i(`Generated built-in rooms`); if (!(await Rooms.generateBuiltinRooms())) log.i(`Generated built-in rooms`);
await Server.CDN.ensureUserDirectory();
try { try {
@@ -130,30 +134,28 @@ try {
valid: false valid: false
} }
type AuthResult = FailedAuth | SuccessfulAuth; type AuthResult = FailedAuth | SuccessfulAuth;
// Please rewrite this for the love of God
const authenticate = async (req: Request) => { const authenticate = async (req: Request) => {
const authHeader = req.headers.get('authorization'); const authHeader = req.headers.get('authorization');
if (!authHeader) return { valid: false } as AuthResult; if (!authHeader) return { valid: false } as AuthResult;
try {
let token: string | undefined; let token: string | undefined;
if (authHeader.substring(0, 6) === 'Bearer') { if (authHeader.substring(0, 6) === 'Bearer') {
const splitToken = authHeader.split(' '); let splitToken;
if (splitToken[1]) token = splitToken[1];
} if (authHeader.includes(', ')) splitToken = authHeader.split(', ')[0].split(' ');
if (authHeader.includes(', ')) { else splitToken = authHeader.split(' ');
const splitToken = authHeader.split(', ');
if (splitToken[1]) token = splitToken[1]; if (splitToken[1]) token = splitToken[1];
} }
try {
if (!token) throw new Error("No token provided"); if (!token) throw new Error("No token provided");
const decodedToken = await decode<ProfileTokenFormat>(token, config.auth.secret, {algorithm: 'HS512'}); const decodedToken = await decode<ProfileTokenFormat>(token, config.auth.secret, {algorithm: 'HS512'});
const schemaResult = ProfileTokenSchema.safeParse(decodedToken); const schemaResult = ProfileTokenSchema.safeParse(decodedToken);
if (!schemaResult.success) return { valid: false } as AuthResult; if (!schemaResult.success) return { valid: false } as AuthResult;
else return { token: decodedToken, valid: true } as AuthResult; else return { token: decodedToken, valid: true } as AuthResult;
} catch (err) { } catch (err) {
log.w(`Authentication failed`); log.w(`Authentication failed: ${(err as Error).message}`);
log.w((err as Error).message);
return { valid: false } as AuthResult; return { valid: false } as AuthResult;
} }
} }
@@ -194,7 +196,11 @@ try {
if (handoff) handoff.complete(); if (handoff) handoff.complete();
const { socket, response } = Deno.upgradeWebSocket(req); const { socket, response } = Deno.upgradeWebSocket(req);
new SignalRSocketHandler(socket, UnifiedProfile.get(authResult.token.sub)); const profile = Server.UnifiedProfile.get(authResult.token.sub);
if (!profile) return new Response(JSON.stringify(APIUtils.genericResponseFormat(true, "Profile not found")), { status: 404 });
new SignalRSocketHandler(socket, profile);
return response; return response;
@@ -219,11 +225,11 @@ try {
http.closeAllConnections(); http.closeAllConnections();
}); });
Deno.addSignalListener("SIGINT", () => { Deno.addSignalListener("SIGINT", () => {
for (const socket of UnifiedProfile.getAllSockets()) socket.sendNotification(PushNotificationId.ModerationQuitGame); // untested for (const socket of Server.UnifiedProfile.getAllSockets()) socket.sendNotification(PushNotificationId.ModerationQuitGame); // untested
}); });
if (!(await UnifiedProfile.existsByName("Coach"))) UnifiedProfile.create({ username: "Coach", id: 1 }); // create Coach id 1 if they do not exist if (!(await Server.UnifiedProfile.existsByName("Coach"))) Server.UnifiedProfile.create({ username: "Coach", id: 1 }); // create Coach id 1 if they do not exist
if (!(await UnifiedProfile.existsByName("Server"))) UnifiedProfile.create({ username: "Server", id: 2 }); // create Server id 2 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
// use these later in development // use these later in development
if (!(await GameConfigs.getGameConfig('splitTestSoftOverrides'))) GameConfigs.setGameConfig('splitTestSoftOverrides', ''); if (!(await GameConfigs.getGameConfig('splitTestSoftOverrides'))) GameConfigs.setGameConfig('splitTestSoftOverrides', '');

View File

@@ -17,9 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { APIUtils, NoBody } from "../../apiutils.ts"; import { APIUtils, NoBody } from "../../apiutils.ts";
import express from "express"; import express from "express";
import UnifiedProfile, { Profile } from "../../data/profiles.ts"; import { Profile } from "../../data/profiles.ts";
import { z } from "zod"; import { z } from "zod";
import { AuthType } from "../../data/users.ts"; import { AuthType } from "../../data/users.ts";
import Server from "../../data/server.ts";
export const route = APIUtils.createRouter("/account"); export const route = APIUtils.createRouter("/account");
@@ -142,7 +143,11 @@ route.router.get('/:id/bio',
rs.sendStatus(400); rs.sendStatus(400);
return; return;
} }
const player = UnifiedProfile.get(parsedId); const player = Server.UnifiedProfile.get(parsedId);
if (!player) {
rs.status(404).json(APIUtils.genericResponseFormat(true, "Profile not found"));
return;
}
rs.json({ rs.json({
accountId: parsedId, accountId: parsedId,

View File

@@ -17,9 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { z } from "zod"; import { z } from "zod";
import { APIUtils, NoBody } from "../../apiutils.ts"; import { APIUtils, NoBody } from "../../apiutils.ts";
import UnifiedProfile from "../../data/profiles.ts";
import { AuthType } from "../../data/users.ts"; import { AuthType } from "../../data/users.ts";
import express from "express"; import express from "express";
import Server from "../../data/server.ts";
export const route = APIUtils.createRouter("/playerReputation"); export const route = APIUtils.createRouter("/playerReputation");
@@ -36,7 +36,13 @@ route.router.get('/v1/:id',
return; return;
} }
rs.json(await UnifiedProfile.get(parsedPlayerId).Reputation.getReputation()); const profile = Server.UnifiedProfile.get(parsedPlayerId);
if (!profile) {
rs.status(404).json(APIUtils.genericResponseFormat(true, "Profile not found"));
return;
}
rs.json(await profile.Reputation.getReputation());
} }
); );
@@ -61,15 +67,21 @@ route.router.post('/v1/bulk',
if (typeof rq.body.Ids == 'object') { if (typeof rq.body.Ids == 'object') {
const reputations = rq.body.Ids const reputations = rq.body.Ids
.map(id => parseInt(id)).filter(id => !isNaN(id)) // parse as int[] and filter out non-numbers .map(id => parseInt(id)).filter(id => !isNaN(id)) // parse as int[] and filter out non-numbers
.map(id => UnifiedProfile.get(id).Reputation.getReputation()); // get all reputations .map(id => Server.UnifiedProfile.get(id)?.Reputation.getReputation()); // get all reputations
rs.json(await Promise.all(reputations)); rs.json(await Promise.all(reputations.filter(val => val instanceof Promise)));
} else { } else {
const id = parseInt(rq.body.Ids); const id = parseInt(rq.body.Ids);
if (isNaN(id)) { if (isNaN(id)) {
rs.sendStatus(400); rs.sendStatus(400);
return; return;
} }
rs.json([await UnifiedProfile.get(id).Reputation.getReputation()]); const profile = Server.UnifiedProfile.get(id);
if (!profile) {
rs.status(404).json(APIUtils.genericResponseFormat(true, "Profile not found"));
return;
}
rs.json([await profile.Reputation.getReputation()]);
} }
}, },

View File

@@ -18,9 +18,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import Logging from "@proxnet/undead-logging"; import Logging from "@proxnet/undead-logging";
import { APIUtils, NoBody } from "../../apiutils.ts"; import { APIUtils, NoBody } from "../../apiutils.ts";
import express from "express"; import express from "express";
import UnifiedProfile from "../../data/profiles.ts";
import { AuthType } from "../../data/users.ts"; import { AuthType } from "../../data/users.ts";
import { z } from "zod"; import { z } from "zod";
import Server from "../../data/server.ts";
const log = new Logging("ProgressionRoute"); const log = new Logging("ProgressionRoute");
@@ -40,7 +40,11 @@ route.router.get('/v1/progression/:id',
return; return;
} }
const profile = UnifiedProfile.get(parsedPlayerId); const profile = Server.UnifiedProfile.get(parsedPlayerId);
if (!profile) {
rs.status(404).json(APIUtils.genericResponseFormat(true, "Profile not found"));
return;
}
const res = { const res = {
PlayerId: profile.getId(), PlayerId: profile.getId(),
Level: await profile.Progression.getLevel(), Level: await profile.Progression.getLevel(),
@@ -72,15 +76,20 @@ route.router.post('/v1/progression/bulk',
if (typeof rq.body.Ids == 'object') { if (typeof rq.body.Ids == 'object') {
const progressions = rq.body.Ids const progressions = rq.body.Ids
.map(id => parseInt(id)).filter(id => !isNaN(id)) // filter out non-numbers .map(id => parseInt(id)).filter(id => !isNaN(id)) // filter out non-numbers
.map(id => UnifiedProfile.get(id).Progression.export()); // get all progressions .map(id => Server.UnifiedProfile.get(id)?.Progression.export()); // get all progressions
rs.json(await Promise.all(progressions)); rs.json(await Promise.all(progressions.filter(val => val instanceof Promise)));
} else { } else {
const id = parseInt(rq.body.Ids); const id = parseInt(rq.body.Ids);
if (isNaN(id)) { if (isNaN(id)) {
rs.sendStatus(400); rs.sendStatus(400);
return; return;
} }
rs.json([await UnifiedProfile.get(id).Progression.export()]); const profile = Server.UnifiedProfile.get(id);
if (!profile) {
rs.status(404).json(APIUtils.genericResponseFormat(true, "Profile not found"));
return;
}
rs.json([await profile.Progression.export()]);
} }
}, },

View File

@@ -18,10 +18,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { z } from "zod"; import { z } from "zod";
import { APIUtils, NoBody } from "../../apiutils.ts"; import { APIUtils, NoBody } from "../../apiutils.ts";
import Rooms from "../../data/content/rooms.ts"; import Rooms from "../../data/content/rooms.ts";
import { RoomDataTypes } from "../../data/content/rooms/DataTypes.ts"; import { FactoryMode, RoomDataTypes, WriteMode } from "../../data/content/rooms/DataTypes.ts";
import { AuthType } from "../../data/users.ts"; import { AuthType } from "../../data/users.ts";
import express from "express"; import express from "express";
import { RoomFactory } from "../../data/content/rooms/RoomFactory.ts"; import { RoomFactory } from "../../data/content/rooms/RoomFactory.ts";
import { SubroomFactory } from "../../data/content/rooms/SubroomFactory.ts";
import Logging from "@proxnet/undead-logging";
const log = new Logging("RoomsRoute");
export const route = APIUtils.createRouter("/rooms"); export const route = APIUtils.createRouter("/rooms");
@@ -188,3 +192,68 @@ route.router.post('/v1/clone',
}, },
); );
const CreatorActionContextScheme = z.object({
IsTeachableMomentRunning: z.boolean()
});
const SaveDataScheme = z.object({
RoomSceneId: z.number(),
RoomDataFilename: z.string().min(6).max(128),
InventionUsages: z.array(z.number()),
CreatorActionContext: CreatorActionContextScheme,
RequestPlayerId: z.number()
});
interface CreatorActionContextBody {
IsTeachableMomentRunning: boolean
}
interface SaveDataBody {
CreatorActionContext: CreatorActionContextBody,
InventionUsages: number[],
RequestPlayerId: number,
RoomDataFilename: string,
RoomSceneId: number
}
route.router.post('/v4/saveData',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
express.json(),
APIUtils.validateRequestBody(SaveDataScheme),
async (rq: express.Request<NoBody, NoBody, SaveDataBody>, rs: express.Response) => {
log.d(`Request to save: '${rq.body.RoomDataFilename}'`);
const currentInstance = rs.locals.profile.getInstance();
if (!currentInstance) {
rs.status(400).json(APIUtils.genericResponseFormat(true, "Player not currently in a room"));
return;
}
const subroomFactory = await new SubroomFactory({
roomId: currentInstance.roomId,
subroomId: rq.body.RoomSceneId,
factoryMode: FactoryMode.Write,
writeMode: WriteMode.Overwrite
}).init();
const splitFilename = rq.body.RoomDataFilename.split('/');
const newFilename = splitFilename[splitFilename.length - 1];
if (!newFilename) {
rs.sendStatus(400);
log.e(`New filename was invalid: '${newFilename}'`);
} else {
subroomFactory.DataBlobName = newFilename;
subroomFactory.addBlobHistory(new Date(), newFilename);
await subroomFactory.write();
rs.json(subroomFactory.export());
currentInstance.dataBlob = newFilename;
currentInstance.updatePlayers();
Rooms.socketUpdateRoom(currentInstance);
}
},
);

View File

@@ -17,7 +17,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { APIUtils, NoBody } from "../../apiutils.ts"; import { APIUtils, NoBody } from "../../apiutils.ts";
import express from "express"; import express from "express";
import UnifiedProfile, { Profile } from "../../data/profiles.ts";
import { decode } from "@gz/jwt"; import { decode } from "@gz/jwt";
import { Config } from "../../config.ts"; import { Config } from "../../config.ts";
import Logging from "@proxnet/undead-logging"; import Logging from "@proxnet/undead-logging";
@@ -26,6 +25,7 @@ import { AuthType } from "../../data/users.ts";
import { Redis } from "../../db.ts"; import { Redis } from "../../db.ts";
import { validVersions } from "../api/versioncheck.ts"; import { validVersions } from "../api/versioncheck.ts";
import { Steam } from "../../data/steam.ts"; import { Steam } from "../../data/steam.ts";
import Server from "../../data/server.ts";
const config = Config.getConfig(); const config = Config.getConfig();
@@ -190,8 +190,8 @@ route.router.post("/token",
rs.locals.user.addAssociatedPlatformId(rq.body.platform_id); rs.locals.user.addAssociatedPlatformId(rq.body.platform_id);
Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, rq.body.platform_id), targetAccount); Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, rq.body.platform_id), targetAccount);
const profile = UnifiedProfile.get(targetAccount); const profile = Server.UnifiedProfile.get(targetAccount);
if (!(await Profile.exists(profile.getId()))) { if (!profile) {
requestFailed(); requestFailed();
return; return;
} }

View File

@@ -16,8 +16,49 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { APIUtils } from "../apiutils.ts"; 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 { route as ConfigRoute } from "./cdn/config.ts"; import { route as ConfigRoute } from "./cdn/config.ts";
import express from "express";
import { Buffer } from "node:buffer";
export const route = APIUtils.createRouter("/cdn"); export const route = APIUtils.createRouter("/cdn");
route.router.use(ConfigRoute.path, ConfigRoute.router); route.router.use(ConfigRoute.path, ConfigRoute.router);
interface CDNGetRouteParams {
type?: string,
name?: string
}
const cdnMiddleware = async (rq: express.Request<CDNGetRouteParams>, rs: express.Response) => {
let file: File | null;
if (rq.params.type && !rq.params.name) file = await Server.CDN.getFile(rq.params.type);
else if (rq.params.name && !rq.params.type) file = await Server.CDN.getFile(rq.params.name);
else if (rq.params.type && rq.params.name) file = await Server.CDN.getFile(`${rq.params.type}/${rq.params.name}`);
else {
rs.sendStatus(400);
return;
}
if (file) rs.type('application/octet-stream').send(Buffer.from(file.data));
else rs.sendStatus(404);
}
route.router.get('/:name',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
cdnMiddleware
);
route.router.get('/:type/:name',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
cdnMiddleware
);

View File

@@ -21,9 +21,9 @@ import express from "express";
import Matchmaking from "../../data/live/base.ts"; import Matchmaking from "../../data/live/base.ts";
import Presence, { PresenceExport } from "../../data/live/presence.ts"; import Presence, { PresenceExport } from "../../data/live/presence.ts";
import { AuthType } from "../../data/users.ts"; import { AuthType } from "../../data/users.ts";
import UnifiedProfile from "../../data/profiles.ts";
import { PlayerStatusVisibility, VRMovementMode } from "../../data/live/types.ts"; import { PlayerStatusVisibility, VRMovementMode } from "../../data/live/types.ts";
import { SettingKey } from "../../data/content/settings.ts"; import { SettingKey } from "../../data/content/settings.ts";
import Server from "../../data/server.ts";
export const route = APIUtils.createRouter('/player'); export const route = APIUtils.createRouter('/player');
@@ -49,14 +49,16 @@ route.router.get('/',
const presExport: PresenceExport[] = []; const presExport: PresenceExport[] = [];
for (const id of ids) { for (const id of ids) {
const pres = await Presence.get(UnifiedProfile.get(id)); const profile = Server.UnifiedProfile.get(id);
if (!profile) continue;
const pres = await Presence.get(profile);
presExport.push(await pres.export()); presExport.push(await pres.export());
} }
rs.json(presExport); rs.json(presExport);
} }
) );
route.router.post('/login', route.router.post('/login',

93
src/routes/storage.ts Normal file
View File

@@ -0,0 +1,93 @@
/* Galvanic Corrosion - Rec Room custom server for communities.
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
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 <https://www.gnu.org/licenses/>. */
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";
export const route = APIUtils.createRouter("/storage");
const multerFileSchema = z.object({
fieldname: z.literal('File'),
originalname: z.string().min(1),
encoding: z.string(),
mimetype: z.string(),
buffer: z.instanceof(Buffer),
size: z.number().positive(),
});
const uploadSchema = z.object({
body: z.object({
FileType: z.string().min(1, 'FileType is required'),
}),
files: z.object({
File: z
.array(multerFileSchema)
.min(1, 'At least one file must be uploaded')
.max(1, 'Only one file is allowed'),
}),
});
const upload = multer({ storage: multer.memoryStorage() });
const uploadFields = upload.fields([
{ name: 'File', maxCount: 1 },
{ name: 'FileType', maxCount: 1 }
]);
interface UploadReqBody {
FileType: string
}
route.router.post('/upload',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
uploadFields,
async (rq: express.Request<NoBody, NoBody, UploadReqBody>, rs: express.Response) => {
const parseResult = uploadSchema.safeParse({
body: rq.body,
files: rq.files
});
if (!parseResult.success) {
rs.status(400).json(APIUtils.genericResponseFormat(true, "Could not parse form"));
return;
}
const file = parseResult.data.files.File[0];
const parsedInt = parseInt(rq.body.FileType);
if (isNaN(parsedInt) || typeof FileType[parsedInt] == 'undefined') {
rs.status(400).json(APIUtils.genericResponseFormat(true, "Could not parse file type"));
return;
}
const name = await Server.CDN.createFile(Uint8Array.from(file.buffer), parsedInt, rs.locals.profile);
rs.json({
filename: name
});
},
);

View File

@@ -17,9 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { z } from "zod"; import { z } from "zod";
import { SocketTarget } from "./targetbase.ts"; import { SocketTarget } from "./targetbase.ts";
import UnifiedProfile, { SelfAccountExport } from "../../data/profiles.ts"; import { SelfAccountExport } from "../../data/profiles.ts";
import { ProfileEvents, ProfileUpdatedEvent } from "../../data/profileevents.ts"; import { ProfileEvents, ProfileUpdatedEvent } from "../../data/profileevents.ts";
import { PushNotificationId } from "../types.ts"; import { PushNotificationId } from "../types.ts";
import Server from "../../data/server.ts";
const ArgumentSchema = z.object({ const ArgumentSchema = z.object({
PlayerIds: z.array(z.number()) PlayerIds: z.array(z.number())
@@ -37,19 +38,23 @@ export class PlayerSocketSubscriptionTarget extends SocketTarget {
this.clearSubscriptions(); this.clearSubscriptions();
for (const id of subs) for (const id of subs) {
this.subscriptions.push({ id: id, callback: UnifiedProfile.get(id) const profile = Server.UnifiedProfile.get(id);
if (!profile) continue;
this.subscriptions.push({ id: id, callback: profile
.on<ProfileUpdatedEvent>(ProfileEvents.BaseUpdated, (async ev => { .on<ProfileUpdatedEvent>(ProfileEvents.BaseUpdated, (async ev => {
const exported = await ev.profile.export(); const exported = await ev.profile.export();
if (exported) this.updateSocket(exported); if (exported) this.updateSocket(exported);
} }
)) }); )) });
}
} }
clearSubscriptions() { clearSubscriptions() {
for (const sub of this.subscriptions) { for (const sub of this.subscriptions) {
const profile = UnifiedProfile.get(sub.id); const profile = Server.UnifiedProfile.get(sub.id);
if (profile)
profile.off(ProfileEvents.BaseUpdated, sub.callback); profile.off(ProfileEvents.BaseUpdated, sub.callback);
} }
} }

View File

@@ -17,7 +17,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { ProfileTokenFormat } from "../data/profiles.ts"; import { ProfileTokenFormat } from "../data/profiles.ts";
// Extend IncomingMessage interface to include custom properties
declare module 'node:http' { declare module 'node:http' {
interface IncomingMessage { interface IncomingMessage {
token?: ProfileTokenFormat; token?: ProfileTokenFormat;