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