Custom Rooms + Server global
All checks were successful
Galvanic Corrosion Cross-Compile / build (push) Successful in 49s
All checks were successful
Galvanic Corrosion Cross-Compile / build (push) Successful in 49s
* Added Storage and room saving (will be moved to events later)
* Moved `UnifiedProfile` to new `Server` object, along with `CDN`
- Will move `Rooms` and others to this later
This commit is contained in:
@@ -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 = <T>(schema: z.ZodSchema<T>) => (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();
|
||||
|
||||
146
src/data/content/cdn.ts
Normal file
146
src/data/content/cdn.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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/>. */
|
||||
|
||||
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;
|
||||
|
||||
@@ -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/>. */
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -348,15 +348,18 @@ class Profile extends EventManager {
|
||||
|
||||
const profiles: Map<number, Profile> = 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;
|
||||
|
||||
28
src/data/server.ts
Normal file
28
src/data/server.ts
Normal 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;
|
||||
44
src/main.ts
44
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<ProfileTokenFormat>(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', '');
|
||||
|
||||
@@ -17,9 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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,
|
||||
|
||||
@@ -17,9 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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()]);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
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()]);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -18,10 +18,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
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<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);
|
||||
}
|
||||
},
|
||||
|
||||
);
|
||||
@@ -17,7 +17,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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/>. */
|
||||
|
||||
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);
|
||||
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
|
||||
|
||||
);
|
||||
@@ -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',
|
||||
|
||||
|
||||
93
src/routes/storage.ts
Normal file
93
src/routes/storage.ts
Normal 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
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
);
|
||||
@@ -17,9 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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<ProfileUpdatedEvent>(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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
|
||||
import { ProfileTokenFormat } from "../data/profiles.ts";
|
||||
|
||||
// Extend IncomingMessage interface to include custom properties
|
||||
declare module 'node:http' {
|
||||
interface IncomingMessage {
|
||||
token?: ProfileTokenFormat;
|
||||
|
||||
Reference in New Issue
Block a user