diff --git a/.gitea/workflows/cross-compile.yaml b/.gitea/workflows/test.yaml similarity index 97% rename from .gitea/workflows/cross-compile.yaml rename to .gitea/workflows/test.yaml index b3f349b..6381808 100644 --- a/.gitea/workflows/cross-compile.yaml +++ b/.gitea/workflows/test.yaml @@ -1,5 +1,4 @@ name: Galvanic Corrosion Cross-Compile - on: push: branches: [master] diff --git a/.gitignore b/.gitignore index 1d25002..9179213 100644 --- a/.gitignore +++ b/.gitignore @@ -129,10 +129,14 @@ dist .yarn/install-state.gz .pnp.* -# galvanic corrosion -build/ -config.json - .vscode + +# galvanic corrosion +/user/ +/config.json +# galvanic corrosion build process +/ver.ts.bak +/build + # used to attach license to each file /append.ts \ No newline at end of file diff --git a/deno.json b/deno.json index ffddbe2..26b7433 100644 --- a/deno.json +++ b/deno.json @@ -14,11 +14,13 @@ "@proxnet/undead-logging": "jsr:@proxnet/undead-logging@^1.2.0", "@types/cookie-parser": "npm:@types/cookie-parser@^1.4.8", "@types/express": "npm:@types/express@^5.0.0", + "@types/multer": "npm:@types/multer@^1.4.12", "@types/validator": "npm:@types/validator@^13.12.2", "cookie-parser": "npm:cookie-parser@^1.4.7", "discord.js": "npm:discord.js@^14.16.3", "express": "npm:express@^4.21.2", "ioredis": "npm:ioredis@^5.5.0", + "multer": "npm:multer@^1.4.5-lts.2", "validator": "npm:validator@^13.12.0", "zod": "npm:zod@^3.24.2" }, diff --git a/deno.lock b/deno.lock index daacd8c..256dcc4 100644 --- a/deno.lock +++ b/deno.lock @@ -11,6 +11,7 @@ "npm:@types/cookie-parser@^1.4.8": "1.4.8_@types+express@5.0.0", "npm:@types/express@*": "5.0.0", "npm:@types/express@5": "5.0.0", + "npm:@types/multer@^1.4.12": "1.4.12", "npm:@types/node@*": "22.5.4", "npm:@types/validator@^13.12.2": "13.12.2", "npm:chalk@^5.3.0": "5.3.0", @@ -18,6 +19,7 @@ "npm:discord.js@^14.16.3": "14.16.3", "npm:express@^4.21.2": "4.21.2", "npm:ioredis@^5.5.0": "5.5.0", + "npm:multer@^1.4.5-lts.2": "1.4.5-lts.2", "npm:validator@^13.12.0": "13.12.0", "npm:zod@^3.24.2": "3.24.2" }, @@ -163,6 +165,12 @@ "@types/mime@1.3.5": { "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": { "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", "dependencies": [ @@ -209,6 +217,9 @@ "negotiator" ] }, + "append-field@1.0.0": { + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "array-flatten@1.1.1": { "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, @@ -229,6 +240,15 @@ "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": { "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, @@ -252,10 +272,19 @@ "cluster-key-slot@1.1.2": { "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": { "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dependencies": [ - "safe-buffer" + "safe-buffer@5.2.1" ] }, "content-type@1.0.5": { @@ -277,6 +306,9 @@ "cookie@0.7.2": { "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" }, + "core-util-is@1.0.3": { + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "debug@2.6.9": { "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dependencies": [ @@ -385,7 +417,7 @@ "proxy-addr", "qs", "range-parser", - "safe-buffer", + "safe-buffer@5.2.1", "send", "serve-static", "setprototypeof", @@ -489,6 +521,9 @@ "ipaddr.js@1.9.1": { "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "isarray@1.0.0": { + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "lodash.defaults@4.2.0": { "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, @@ -529,15 +564,40 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "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": { "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "ms@2.1.3": { "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": { "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, + "object-assign@4.1.1": { + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, "object-inspect@1.13.3": { "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==" }, @@ -553,6 +613,9 @@ "path-to-regexp@0.1.12": { "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, + "process-nextick-args@2.0.1": { + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "proxy-addr@2.0.7": { "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dependencies": [ @@ -578,6 +641,18 @@ "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": { "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==" }, @@ -587,6 +662,9 @@ "redis-errors" ] }, + "safe-buffer@5.1.2": { + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "safe-buffer@5.2.1": { "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, @@ -665,6 +743,15 @@ "statuses@2.0.1": { "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": { "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" }, @@ -681,6 +768,9 @@ "mime-types" ] }, + "typedarray@0.0.6": { + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "undici-types@6.19.8": { "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, @@ -690,6 +780,9 @@ "unpipe@1.0.0": { "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, + "util-deprecate@1.0.2": { + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "utils-merge@1.0.1": { "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" }, @@ -700,11 +793,10 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "ws@8.18.0": { - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "optionalPeers": [ - "bufferutil@^4.0.1", - "utf-8-validate@>=5.0.2" - ] + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==" + }, + "xtend@4.0.2": { + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "zod@3.24.2": { "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==" @@ -857,11 +949,13 @@ "jsr:@proxnet/undead-logging@^1.2.0", "npm:@types/cookie-parser@^1.4.8", "npm:@types/express@5", + "npm:@types/multer@^1.4.12", "npm:@types/validator@^13.12.2", "npm:cookie-parser@^1.4.7", "npm:discord.js@^14.16.3", "npm:express@^4.21.2", "npm:ioredis@^5.5.0", + "npm:multer@^1.4.5-lts.2", "npm:validator@^13.12.0", "npm:zod@^3.24.2" ] diff --git a/src/apiutils.ts b/src/apiutils.ts index 7d35d4e..e40bb92 100644 --- a/src/apiutils.ts +++ b/src/apiutils.ts @@ -21,9 +21,10 @@ import Logging from "@proxnet/undead-logging"; import { decode } from "@gz/jwt"; import { Config } from "./config.ts"; import { AuthType, User, UserTokenFormat } from "./data/users.ts"; -import UnifiedProfile, { ProfileTokenFormat } from "./data/profiles.ts"; +import { ProfileTokenFormat } from "./data/profiles.ts"; import z from "zod"; import Matchmaking from "./data/live/base.ts"; +import Server from "./data/server.ts"; const config = Config.getConfig(); @@ -101,7 +102,7 @@ export const validateQuery = (schema: z.ZodSchema) => (rq: express.Request } }; -type genericResponse = { +export type genericResponse = { failure: boolean, errors?: object, // zod only message?: string, @@ -130,7 +131,7 @@ export function genericResponse( rs.json({ failure: failure, errors: errors, message: msg, data: data }); }; } -type RecNetResponse = { +export type RecNetResponse = { success: boolean; error?: string; }; @@ -317,7 +318,13 @@ export async function Authentication( ].includes(false); if (valid) { 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; nxt(); diff --git a/src/data/content/cdn.ts b/src/data/content/cdn.ts new file mode 100644 index 0000000..a7a7679 --- /dev/null +++ b/src/data/content/cdn.ts @@ -0,0 +1,146 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import Logging from "@proxnet/undead-logging"; +import { 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, + 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, 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; + } + } + +} \ No newline at end of file diff --git a/src/data/content/rooms.ts b/src/data/content/rooms.ts index d6a11ee..624dd37 100644 --- a/src/data/content/rooms.ts +++ b/src/data/content/rooms.ts @@ -22,6 +22,8 @@ import { BuiltinRoom, FactoryMode, IntegratedRoomScene, RoomAccessibility, RoomD import { RoomFactory } from "./rooms/RoomFactory.ts"; import { SubroomFactory } from "./rooms/SubroomFactory.ts"; import { RootPath } from "../../path.ts"; +import { Instance } from "../live/instances.ts"; +import { PushNotificationId } from "../../socket/types.ts"; const log = new Logging("Rooms"); @@ -299,6 +301,18 @@ class RoomsBase { 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(); diff --git a/src/data/live/instances.ts b/src/data/live/instances.ts index 58d793b..88ad94f 100644 --- a/src/data/live/instances.ts +++ b/src/data/live/instances.ts @@ -16,13 +16,14 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import Logging from "@proxnet/undead-logging"; -import UnifiedProfile, { Profile } from "../profiles.ts"; +import { Profile } from "../profiles.ts"; import { RoomInstance, InstanceOptions } from "./types.ts"; import { Config } from "../../config.ts"; import Presence from "./presence.ts"; import { RoomFactory } from "../content/rooms/RoomFactory.ts"; import { RoomDataTypes } from "../content/rooms/DataTypes.ts"; import { PushNotificationId } from "../../socket/types.ts"; +import Server from "../server.ts"; const log = new Logging("Instances"); @@ -55,13 +56,14 @@ export class Instance { async init(options: InstanceOptions) { 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; if (scene.Name == 'Home' || scene.Name === options.Room.Room.Name) instanceName = `^${options.Room.Room.Name}`; else instanceName = `^${options.Room.Room.Name}.${scene.Name}`; if (options.IsDorm) { - const dormCreatorPlayer = 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(); if (player) instanceName = `@${player.displayName}'s Dorm`; } @@ -95,6 +97,10 @@ export class Instance { player.setInstance(null); } + updatePlayers() { + for (const player of this.#players.values()) player.getSocketHandler()?.sendNotification(PushNotificationId.SubscriptionUpdateGameSession, this.snapshot()); + } + async addPlayer(player: Profile) { const currentInstance = player.getInstance(); if (currentInstance && currentInstance.equalInstance(this)) return; diff --git a/src/data/profile/relationships.ts b/src/data/profile/relationships.ts index 1b20e37..7eb62d7 100644 --- a/src/data/profile/relationships.ts +++ b/src/data/profile/relationships.ts @@ -16,8 +16,9 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { Redis } from "../../db.ts"; -import UnifiedProfile, { Profile } from "../profiles.ts"; +import { Profile } from "../profiles.ts"; import { ProfileContentManager } from "./base/profilemanagerbase.ts"; +import Server from "../server.ts"; enum RelationshipType { None, @@ -223,7 +224,11 @@ export class ProfileRelationshipManager extends ProfileContentManager { async ignoreAllAssociatedPlatformUsers(platformid: string) { const ids = (await Redis.Database.smembers(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, platformid))).map(val => parseInt(val)).filter(val => !isNaN(val)); - for (const id of ids) this.setPlayerIgnored(UnifiedProfile.get(id)); + for (const id of ids) { + const profile = Server.UnifiedProfile.get(id); + if (!profile) continue; + this.setPlayerIgnored(profile); + } } } \ No newline at end of file diff --git a/src/data/profiles.ts b/src/data/profiles.ts index bd09dee..b6f92bf 100644 --- a/src/data/profiles.ts +++ b/src/data/profiles.ts @@ -348,15 +348,18 @@ class Profile extends EventManager { const profiles: Map = new Map() -// Control what is available to references -class UnifiedProfileBase { +export class UnifiedProfileBase { get(id: number) { let profile = profiles.get(id); if (!profile) { - const inst = new Profile(id); - profiles.set(id, inst); - profile = inst; + try { + const inst = new Profile(id); + profiles.set(id, inst); + profile = inst; + } catch { + return null; + } } return profile; } @@ -384,7 +387,7 @@ class UnifiedProfileBase { } -const UnifiedProfile = new UnifiedProfileBase(); +const UnifiedProfile = ""; export { Profile }; export default UnifiedProfile; diff --git a/src/data/server.ts b/src/data/server.ts new file mode 100644 index 0000000..a6d5a75 --- /dev/null +++ b/src/data/server.ts @@ -0,0 +1,28 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +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; diff --git a/src/main.ts b/src/main.ts index b23d52a..169cb99 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,16 +21,16 @@ import { Database } from "./db.ts"; import { APIUtils, ProfileTokenSchema } from "./apiutils.ts"; import { Discord } from "./discord.ts"; import { generateRandomString } from "./apiutils.ts"; -// @ts-types = "npm:@types/express" import express from "express"; 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 { SignalRSocketHandler } from "./socket/socket.ts"; import { GameConfigs } from "./data/config.ts"; import { getVersion } from "./ver.ts"; import Rooms from "./data/content/rooms.ts"; import { PushNotificationId } from "./socket/types.ts"; +import Server from "./data/server.ts"; const instanceId = generateRandomString(64); @@ -71,6 +71,7 @@ app.disable("x-powered-by"); app.use( (rq: express.Request, rs: express.Response, nxt: express.NextFunction) => { rs.setHeader("Instance", instanceId); + rs.setHeader('Access-Control-Allow-Origin', '*'); rs.locals.reqId = generateRandomString(12); log.n(`${rs.locals.reqId} ${APIUtils.getSrcIpDefault(rq)} ${rq.method} ${rq.originalUrl}`); nxt(); @@ -96,6 +97,7 @@ const imgRouter = await import("./routes/img.ts"); const matchRouter = await import("./routes/match.ts"); const notifyRouter = await import("./socket/route.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(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(notifyRouter.route.path, notifyRouter.route.router); app.use(cdnRouter.route.path, cdnRouter.route.router); +app.use(storageRouter.route.path, storageRouter.route.router); // 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`); +await Server.CDN.ensureUserDirectory(); try { @@ -130,30 +134,28 @@ try { valid: false } type AuthResult = FailedAuth | SuccessfulAuth; - // Please rewrite this for the love of God const authenticate = async (req: Request) => { const authHeader = req.headers.get('authorization'); if (!authHeader) return { valid: false } as AuthResult; - let token: string | undefined; - if (authHeader.substring(0, 6) === 'Bearer') { - const splitToken = authHeader.split(' '); - if (splitToken[1]) token = splitToken[1]; - } - if (authHeader.includes(', ')) { - const splitToken = authHeader.split(', '); - if (splitToken[1]) token = splitToken[1]; - } - try { + let token: string | undefined; + if (authHeader.substring(0, 6) === 'Bearer') { + let splitToken; + + if (authHeader.includes(', ')) splitToken = authHeader.split(', ')[0].split(' '); + else splitToken = authHeader.split(' '); + + if (splitToken[1]) token = splitToken[1]; + } + if (!token) throw new Error("No token provided"); const decodedToken = await decode(token, config.auth.secret, {algorithm: 'HS512'}); const schemaResult = ProfileTokenSchema.safeParse(decodedToken); if (!schemaResult.success) return { valid: false } as AuthResult; else return { token: decodedToken, valid: true } as AuthResult; } catch (err) { - log.w(`Authentication failed`); - log.w((err as Error).message); + log.w(`Authentication failed: ${(err as Error).message}`); return { valid: false } as AuthResult; } } @@ -194,7 +196,11 @@ try { if (handoff) handoff.complete(); 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; @@ -219,11 +225,11 @@ try { http.closeAllConnections(); }); 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 UnifiedProfile.existsByName("Server"))) UnifiedProfile.create({ username: "Server", id: 2 }); // create Server id 2 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 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 if (!(await GameConfigs.getGameConfig('splitTestSoftOverrides'))) GameConfigs.setGameConfig('splitTestSoftOverrides', ''); diff --git a/src/routes/account/account.ts b/src/routes/account/account.ts index e51b642..c5f5035 100644 --- a/src/routes/account/account.ts +++ b/src/routes/account/account.ts @@ -17,9 +17,10 @@ along with this program. If not, see . */ import { APIUtils, NoBody } from "../../apiutils.ts"; import express from "express"; -import UnifiedProfile, { Profile } from "../../data/profiles.ts"; +import { Profile } from "../../data/profiles.ts"; import { z } from "zod"; import { AuthType } from "../../data/users.ts"; +import Server from "../../data/server.ts"; export const route = APIUtils.createRouter("/account"); @@ -142,7 +143,11 @@ route.router.get('/:id/bio', rs.sendStatus(400); 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({ accountId: parsedId, diff --git a/src/routes/api/playerReputation.ts b/src/routes/api/playerReputation.ts index a5769e5..ea67530 100644 --- a/src/routes/api/playerReputation.ts +++ b/src/routes/api/playerReputation.ts @@ -17,9 +17,9 @@ along with this program. If not, see . */ import { z } from "zod"; import { APIUtils, NoBody } from "../../apiutils.ts"; -import UnifiedProfile from "../../data/profiles.ts"; import { AuthType } from "../../data/users.ts"; import express from "express"; +import Server from "../../data/server.ts"; export const route = APIUtils.createRouter("/playerReputation"); @@ -35,8 +35,14 @@ route.router.get('/v1/:id', rs.json(APIUtils.genericResponseFormat(true, 'The player ID was invalid.')); return; } + + const profile = Server.UnifiedProfile.get(parsedPlayerId); + if (!profile) { + rs.status(404).json(APIUtils.genericResponseFormat(true, "Profile not found")); + return; + } - rs.json(await UnifiedProfile.get(parsedPlayerId).Reputation.getReputation()); + rs.json(await profile.Reputation.getReputation()); } ); @@ -61,15 +67,21 @@ route.router.post('/v1/bulk', if (typeof rq.body.Ids == 'object') { const reputations = rq.body.Ids .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 - rs.json(await Promise.all(reputations)); + .map(id => Server.UnifiedProfile.get(id)?.Reputation.getReputation()); // get all reputations + rs.json(await Promise.all(reputations.filter(val => val instanceof Promise))); } else { const id = parseInt(rq.body.Ids); if (isNaN(id)) { rs.sendStatus(400); 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()]); } }, diff --git a/src/routes/api/players.ts b/src/routes/api/players.ts index c535248..3a62dd7 100644 --- a/src/routes/api/players.ts +++ b/src/routes/api/players.ts @@ -18,9 +18,9 @@ along with this program. If not, see . */ import Logging from "@proxnet/undead-logging"; import { APIUtils, NoBody } from "../../apiutils.ts"; import express from "express"; -import UnifiedProfile from "../../data/profiles.ts"; import { AuthType } from "../../data/users.ts"; import { z } from "zod"; +import Server from "../../data/server.ts"; const log = new Logging("ProgressionRoute"); @@ -40,7 +40,11 @@ route.router.get('/v1/progression/:id', 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 = { PlayerId: profile.getId(), Level: await profile.Progression.getLevel(), @@ -72,15 +76,20 @@ route.router.post('/v1/progression/bulk', if (typeof rq.body.Ids == 'object') { const progressions = rq.body.Ids .map(id => parseInt(id)).filter(id => !isNaN(id)) // filter out non-numbers - .map(id => UnifiedProfile.get(id).Progression.export()); // get all progressions - rs.json(await Promise.all(progressions)); + .map(id => Server.UnifiedProfile.get(id)?.Progression.export()); // get all progressions + rs.json(await Promise.all(progressions.filter(val => val instanceof Promise))); } else { const id = parseInt(rq.body.Ids); if (isNaN(id)) { rs.sendStatus(400); 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()]); } }, diff --git a/src/routes/api/rooms.ts b/src/routes/api/rooms.ts index 741dfe9..139db41 100644 --- a/src/routes/api/rooms.ts +++ b/src/routes/api/rooms.ts @@ -18,10 +18,14 @@ along with this program. If not, see . */ import { z } from "zod"; import { APIUtils, NoBody } from "../../apiutils.ts"; import Rooms from "../../data/content/rooms.ts"; -import { RoomDataTypes } from "../../data/content/rooms/DataTypes.ts"; +import { FactoryMode, RoomDataTypes, WriteMode } from "../../data/content/rooms/DataTypes.ts"; import { AuthType } from "../../data/users.ts"; import express from "express"; import { RoomFactory } from "../../data/content/rooms/RoomFactory.ts"; +import { SubroomFactory } from "../../data/content/rooms/SubroomFactory.ts"; +import Logging from "@proxnet/undead-logging"; + +const log = new Logging("RoomsRoute"); export const route = APIUtils.createRouter("/rooms"); @@ -187,4 +191,69 @@ 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, 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); + } + }, + ); \ No newline at end of file diff --git a/src/routes/auth/connect.ts b/src/routes/auth/connect.ts index 062299b..829f2b8 100644 --- a/src/routes/auth/connect.ts +++ b/src/routes/auth/connect.ts @@ -17,7 +17,6 @@ along with this program. If not, see . */ import { APIUtils, NoBody } from "../../apiutils.ts"; import express from "express"; -import UnifiedProfile, { Profile } from "../../data/profiles.ts"; import { decode } from "@gz/jwt"; import { Config } from "../../config.ts"; import Logging from "@proxnet/undead-logging"; @@ -26,6 +25,7 @@ import { AuthType } from "../../data/users.ts"; import { Redis } from "../../db.ts"; import { validVersions } from "../api/versioncheck.ts"; import { Steam } from "../../data/steam.ts"; +import Server from "../../data/server.ts"; const config = Config.getConfig(); @@ -190,8 +190,8 @@ route.router.post("/token", rs.locals.user.addAssociatedPlatformId(rq.body.platform_id); Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.PlatformAssociations, rq.body.platform_id), targetAccount); - const profile = UnifiedProfile.get(targetAccount); - if (!(await Profile.exists(profile.getId()))) { + const profile = Server.UnifiedProfile.get(targetAccount); + if (!profile) { requestFailed(); return; } diff --git a/src/routes/cdn.ts b/src/routes/cdn.ts index d3e41b5..1c5809a 100644 --- a/src/routes/cdn.ts +++ b/src/routes/cdn.ts @@ -16,8 +16,49 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ import { APIUtils } from "../apiutils.ts"; +import { 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 express from "express"; +import { Buffer } from "node:buffer"; export const route = APIUtils.createRouter("/cdn"); -route.router.use(ConfigRoute.path, ConfigRoute.router); \ No newline at end of file +route.router.use(ConfigRoute.path, ConfigRoute.router); + +interface CDNGetRouteParams { + type?: string, + name?: string +} + +const cdnMiddleware = async (rq: express.Request, 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 + +); \ No newline at end of file diff --git a/src/routes/match/player.ts b/src/routes/match/player.ts index ac11e24..e773ebe 100644 --- a/src/routes/match/player.ts +++ b/src/routes/match/player.ts @@ -21,9 +21,9 @@ import express from "express"; import Matchmaking from "../../data/live/base.ts"; import Presence, { PresenceExport } from "../../data/live/presence.ts"; import { AuthType } from "../../data/users.ts"; -import UnifiedProfile from "../../data/profiles.ts"; import { PlayerStatusVisibility, VRMovementMode } from "../../data/live/types.ts"; import { SettingKey } from "../../data/content/settings.ts"; +import Server from "../../data/server.ts"; export const route = APIUtils.createRouter('/player'); @@ -49,14 +49,16 @@ route.router.get('/', const presExport: PresenceExport[] = []; 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()); } rs.json(presExport); } -) +); route.router.post('/login', diff --git a/src/routes/storage.ts b/src/routes/storage.ts new file mode 100644 index 0000000..ef755b5 --- /dev/null +++ b/src/routes/storage.ts @@ -0,0 +1,93 @@ +/* Galvanic Corrosion - Rec Room custom server for communities. + +Copyright (C) 2025 @zombieb (Discord / proxnet Gitea) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + +import { z } from "zod"; +import { 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, 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 + }); + + }, + +); \ No newline at end of file diff --git a/src/socket/targets/SubscribeToPlayers.ts b/src/socket/targets/SubscribeToPlayers.ts index 6693869..f94fb47 100644 --- a/src/socket/targets/SubscribeToPlayers.ts +++ b/src/socket/targets/SubscribeToPlayers.ts @@ -17,9 +17,10 @@ along with this program. If not, see . */ import { z } from "zod"; 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 { PushNotificationId } from "../types.ts"; +import Server from "../../data/server.ts"; const ArgumentSchema = z.object({ PlayerIds: z.array(z.number()) @@ -37,20 +38,24 @@ export class PlayerSocketSubscriptionTarget extends SocketTarget { this.clearSubscriptions(); - for (const id of subs) - this.subscriptions.push({ id: id, callback: UnifiedProfile.get(id) + for (const id of subs) { + const profile = Server.UnifiedProfile.get(id); + if (!profile) continue; + this.subscriptions.push({ id: id, callback: profile .on(ProfileEvents.BaseUpdated, (async ev => { const exported = await ev.profile.export(); if (exported) this.updateSocket(exported); } - )) }); + )) }); + } } clearSubscriptions() { for (const sub of this.subscriptions) { - const profile = UnifiedProfile.get(sub.id); - profile.off(ProfileEvents.BaseUpdated, sub.callback); + const profile = Server.UnifiedProfile.get(sub.id); + if (profile) + profile.off(ProfileEvents.BaseUpdated, sub.callback); } } diff --git a/src/types/http.ts b/src/types/http.ts index 618eba5..5b2908e 100644 --- a/src/types/http.ts +++ b/src/types/http.ts @@ -17,7 +17,6 @@ along with this program. If not, see . */ import { ProfileTokenFormat } from "../data/profiles.ts"; -// Extend IncomingMessage interface to include custom properties declare module 'node:http' { interface IncomingMessage { token?: ProfileTokenFormat; diff --git a/ver.ts b/ver.ts index 3f851eb..965c740 100644 --- a/ver.ts +++ b/ver.ts @@ -15,4 +15,4 @@ GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -export const Version = 'development'; \ No newline at end of file +export const Version = '0.1.0-ac2701acec15'; \ No newline at end of file