From 1cfd0426dd3af02ae77407f12d0a73d78bf54607 Mon Sep 17 00:00:00 2001 From: zombieb Date: Wed, 2 Apr 2025 23:56:18 -0400 Subject: [PATCH] FLINT AND STEEL!!!! THE NETHER!!!!!! RELEASE!!!!!!!!! * Account bio support (fetch only route right now) * Room cloning fixes - Dorm Room cloning is still broken * Instance changing fixes * Presence: VRMovementMode and StatusVisibility updates automatically * Routes for the above two properties * Settings can take numbers, too (enums) * No microtransations in my game (parental controls) * A whole lotta routes for various unfinished but planned features - Equipment - Consumables - Objectives - Checklist (orientation rewards) - Objectives (three daily tasks) - Image metadata - Community Board - Player Events - Storefronts * Matchmaking instance querying - Empty instances are not yet cleared * Avatar items, saved avatars, save current avatar routes * No loading screen tips for now * Send presence at an interval over the socket - Error FROSTBITE is reported in the game logs during bootup sometimes. Maybe due to the lack of ping messages? * Socket push notifications Note to self: Set up deno compilation in runners on gitea --- res/rooms.json | 4 +- src/data/content/rooms.ts | 28 +++++----- src/data/content/storefronts.ts | 31 +++++++++++ src/data/live/base.ts | 6 +-- src/data/live/instances.ts | 7 ++- src/data/live/presence.ts | 30 +++++++---- src/data/live/types.ts | 2 +- src/data/profile/avatar.ts | 2 +- src/data/profile/settings.ts | 4 +- src/data/profiles.ts | 12 ++++- src/db.ts | 1 + src/routes/account.ts | 2 + src/routes/account/account.ts | 37 +++++++++++--- src/routes/account/parentalcontrol.ts | 35 +++++++++++++ src/routes/api.ts | 18 ++++++- src/routes/api/avatar.ts | 47 +++++++++++++++-- src/routes/api/checklist.ts | 30 +++++++++++ src/routes/api/communityboard.ts | 51 ++++++++++++++++++ src/routes/api/consumables.ts | 30 +++++++++++ src/routes/api/equipment.ts | 30 +++++++++++ src/routes/api/gameconfigs.ts | 4 +- src/routes/api/images.ts | 30 +++++++++++ src/routes/api/messages.ts | 4 +- src/routes/api/objectives.ts | 35 +++++++++++++ src/routes/api/playerevents.ts | 44 ++++++++++++++++ src/routes/api/playersubscriptions.ts | 4 +- src/routes/api/rooms.ts | 74 ++++++++++++++++++++++++++- src/routes/api/storefronts.ts | 42 +++++++++++++++ src/routes/cdn/config.ts | 4 +- src/routes/match.ts | 4 +- src/routes/match/goto.ts | 4 ++ src/routes/match/player.ts | 48 ++++++++++++++++- src/routes/match/room.ts | 56 ++++++++++++++++++++ src/socket/socket.ts | 22 ++++++++ src/socket/types.ts | 40 ++++++++++++++- 35 files changed, 758 insertions(+), 64 deletions(-) create mode 100644 src/data/content/storefronts.ts create mode 100644 src/routes/account/parentalcontrol.ts create mode 100644 src/routes/api/checklist.ts create mode 100644 src/routes/api/communityboard.ts create mode 100644 src/routes/api/consumables.ts create mode 100644 src/routes/api/equipment.ts create mode 100644 src/routes/api/images.ts create mode 100644 src/routes/api/objectives.ts create mode 100644 src/routes/api/playerevents.ts create mode 100644 src/routes/api/storefronts.ts create mode 100644 src/routes/match/room.ts diff --git a/res/rooms.json b/res/rooms.json index 2a391d2..a0aee40 100644 --- a/res/rooms.json +++ b/res/rooms.json @@ -27,10 +27,10 @@ { "Name": "DormRoom", "ReplicationId": "68251132-5662-5c34-08b1-4a830a27955b", - "Description": "Your private room", + "Description": "Your private room.", "Accessibility": 2, "SupportsLevelVoting": false, - "CloningAllowed": false, + "CloningAllowed": true, "SupportsScreens": true, "SupportsWalkVR": true, "SupportsTeleportVR": true, diff --git a/src/data/content/rooms.ts b/src/data/content/rooms.ts index ed097d9..c044d62 100644 --- a/src/data/content/rooms.ts +++ b/src/data/content/rooms.ts @@ -131,7 +131,7 @@ class RoomsBase { Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Name, details.Room.Name), Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Description, details.Room.Description), Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CreatorPlayerId, details.Room.CreatorPlayerId), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.ImageName, `DefaultProfileImage.png`), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.ImageName, details.Room.ImageName), Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.State, details.Room.State), Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.Accessibility, details.Room.Accessibility), Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.SupportsLevelVoting, `${details.Room.SupportsLevelVoting}`), @@ -144,7 +144,7 @@ class RoomsBase { Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.AllowsJuniors, `${details.Room.AllowsJuniors}`), Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.RoomWarningMask, details.Room.RoomWarningMask), Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.CustomRoomWarning, details.Room.CustomRoomWarning), - Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.DisableMicAutoMute, `${details.Room.DisableMicAutoMute}`), + Redis.Database.hset(roomMetaRootKey, this.roomMetaKeys.DisableMicAutoMute, `${details.Room.DisableMicAutoMute ? details.Room.DisableMicAutoMute : 'null'}`), ]); for (const subroom of details.Scenes) { @@ -159,7 +159,7 @@ class RoomsBase { await Promise.all([ Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.RoomSceneLocationId, subroom.RoomSceneLocationId), - Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.Name, subroom.RoomSceneLocationId), + Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.Name, subroom.Name), Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.IsSandbox, `${subroom.IsSandbox}`), Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.DataBlobName, subroom.DataBlobName), Redis.Database.hset(subRootMetaKey, this.subroomMetaKeys.MaxPlayers, subroom.MaxPlayers), @@ -168,9 +168,9 @@ class RoomsBase { ]); await Redis.Database.sadd(Redis.buildKey(Redis.KeyGroups.Rooms.Root, details.Room.RoomId.toString(), this.roomRootKeys.Subrooms), newSubId); } - } - + await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Room_Names, details.Room.Name), details.Room.RoomId); + } async cloneRoom(roomid: number, newname: string, newowner: Profile, dorm?: boolean) { const canBeClonedRaw = await Redis.Database.hget(Redis.buildKey( @@ -188,7 +188,7 @@ class RoomsBase { if (!canBeCloned) return null; const beforeRoom = await Rooms.get(roomid); // room must exist if (!beforeRoom || !beforeRoom.Room.CloningAllowed) return null; // room must be cloneable - if (await Rooms.getByName(newname)) return null; // room name cannot be taken + if (await Rooms.getByName(newname) && beforeRoom.Room.Name !== 'DormRoom') return null; // room name cannot be taken const newId = await this.#getAvailableRoomId(); beforeRoom.Room.CreatorPlayerId = newowner.getId(); @@ -197,9 +197,9 @@ class RoomsBase { if (dorm) { beforeRoom.Room.IsAGRoom = true; beforeRoom.Room.IsDormRoom = true; + beforeRoom.Room.CloningAllowed = false; } - await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Room_Names, newname), newId); await Rooms.#setRoom(beforeRoom); return beforeRoom; @@ -217,17 +217,17 @@ class RoomsBase { const baseDorm = await Rooms.getByName("DormRoom"); - log.d('got base dorm'); + log.d('Base dorm existed'); if (!baseDorm) return null; - log.d('base dorm is not null'); + log.d('Base dorm was not null'); const newDorm = await this.cloneRoom(baseDorm.Room.RoomId, "DormRoom", player, true); + log.d('New dorm existed'); + if (!newDorm) return null; + log.d('New dorm was not null'); await Redis.Database.hset(Redis.buildKey( Redis.KeyGroups.Rooms.Root, Redis.KeyGroups.Rooms.PlayerDorms - ), player.getId().toString(), baseDorm.Room.RoomId); - log.d('got new dorm'); - if (!newDorm) return null; - log.d('new dorm is not null'); + ), player.getId().toString(), newDorm.Room.RoomId); return newDorm; } @@ -249,7 +249,7 @@ class RoomsBase { SupportsLevelVoting: builtinRoom.SupportsLevelVoting, IsAGRoom: true, IsDormRoom: builtinRoom.Name == 'DormRoom', - CloningAllowed: builtinRoom.CloningAllowed, + CloningAllowed: builtinRoom.Name == 'DormRoom' ? true : builtinRoom.CloningAllowed, SupportsScreens: builtinRoom.SupportsScreens, SupportsWalkVR: builtinRoom.SupportsWalkVR, SupportsTeleportVR: builtinRoom.SupportsTeleportVR, diff --git a/src/data/content/storefronts.ts b/src/data/content/storefronts.ts new file mode 100644 index 0000000..b43ef91 --- /dev/null +++ b/src/data/content/storefronts.ts @@ -0,0 +1,31 @@ +/* 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 . */ + +export enum StorefrontTypes { + None, + LaserTag, + RecCenter, + Watch, + Quest_LostSkulls = 100, + Quest_Dracula, + RecRoyale = 200, + Cafe = 300, + Paintball = 400, + Bowling = 500, + StuntRunner = 600, + DormMirror = 700 +} \ No newline at end of file diff --git a/src/data/live/base.ts b/src/data/live/base.ts index 1cc4efd..d4db6a6 100644 --- a/src/data/live/base.ts +++ b/src/data/live/base.ts @@ -53,7 +53,7 @@ class MatchmakingBase { loginLocks.delete(prof.getId()); } - async matchmake(options: MatchmakingOptions) { + async matchmake(options: MatchmakingOptions): Promise { if (options.instanceId) { @@ -65,7 +65,7 @@ class MatchmakingBase { if (instance.isFull) return { errorCode: MatchmakingErrorCode.InsufficientSpace } else if (instance.isPrivate) return { errorCode: MatchmakingErrorCode.RoomInstanceIsPrivate }; - Instances.setPlayerInstance(options.profile, instance); + await Instances.setPlayerInstance(options.profile, instance); return { errorCode: MatchmakingErrorCode.Success, roomInstance: instance }; } @@ -96,7 +96,7 @@ class MatchmakingBase { } else { - Instances.setPlayerInstance(options.profile, foundInstance); + await Instances.setPlayerInstance(options.profile, foundInstance); return { errorCode: MatchmakingErrorCode.Success, roomInstance: foundInstance }; } diff --git a/src/data/live/instances.ts b/src/data/live/instances.ts index 96b3bab..6016cad 100644 --- a/src/data/live/instances.ts +++ b/src/data/live/instances.ts @@ -19,6 +19,7 @@ import Logging from "@proxnet/undead-logging"; import { Profile } from "../profiles.ts"; import { RoomInstance, InstanceOptions } from "./types.ts"; import { Config } from "../../config.ts"; +import Presence from "./presence.ts"; const log = new Logging("Instances"); @@ -150,7 +151,7 @@ class InstancesBase { * * Synchronizes profile instance to `instance` and adds player to instance. */ - setPlayerInstance(player: Profile, instance: RoomInstance) { + async setPlayerInstance(player: Profile, instance: RoomInstance) { const currentInstance = player.getInstance(); if (currentInstance === instance) return; @@ -161,7 +162,11 @@ class InstancesBase { if (this.instanceCanFitPlayer(instance)) { this.getInstancePlayers(instance).add(player); + player.setInstance(instance); + const pres = await Presence.get(player); + pres.update(); + this.updateSingleInstanceIsFull(instance); } else log.w(`Instance ${instance.roomInstanceId} is full. Cannot add player ${player.getId()}`); } diff --git a/src/data/live/presence.ts b/src/data/live/presence.ts index a3e8b47..e518161 100644 --- a/src/data/live/presence.ts +++ b/src/data/live/presence.ts @@ -62,31 +62,43 @@ class PlayerPresence { playerId: number; statusVisibility: PlayerStatusVisibility; deviceClass: DeviceClass; - vrMovementMode: VRMovementMode; + 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.Settings.getSetting(SettingKey.PlayerStatusVisibility)); if (visibilityResult.success) this.statusVisibility = visibilityResult.data; } + async updateVRMovementMode() { + const VRMovementModeEnum = z.nativeEnum(VRMovementMode); + + const modeResult = VRMovementModeEnum.safeParse(await this.#profile.Settings.getSetting(SettingKey.VRMovementMode)); + if (modeResult.success) this.vrMovementMode = modeResult.data; + } + async update() { this.updateOffline(); - if (!this.offline) await this.updateStatusVisibility(); - else this.statusVisibility = PlayerStatusVisibility.Offline; + if (!this.offline) { + await this.updateStatusVisibility(); + await this.updateVRMovementMode(); + } + else { + this.vrMovementMode = undefined; + this.statusVisibility = PlayerStatusVisibility.Offline; + } this.deviceClass = await this.#profile.getKnownDeviceClass(); - this.vrMovementMode = await this.#profile.getVRMovementMode(); + this.roomInstance = this.#profile.getInstance(); } /** - * Export presence object. Please make sure to update values with `Presence.update()` (async) before calling this. + * Export presence */ async export() { await this.update(); @@ -97,7 +109,7 @@ class PlayerPresence { deviceClass: this.deviceClass, vrMovementMode: this.vrMovementMode } - return Object.assign({}, exp); // hard clone + return exp; } updateOffline() { @@ -118,10 +130,8 @@ class PresenceBase { */ async getAllPresences() { const presSet: Set = new Set(); - for (const pres of presence.values()) { - await pres.update(); + for (const pres of presence.values()) presSet.add(await pres.export()); - } return presSet; } diff --git a/src/data/live/types.ts b/src/data/live/types.ts index 9ab5270..b9ae7fa 100644 --- a/src/data/live/types.ts +++ b/src/data/live/types.ts @@ -15,7 +15,7 @@ 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 { IntegratedRoomScene, RoomDetails } from "../content/roomtypes.ts"; +import { RoomDetails } from "../content/roomtypes.ts"; import { Profile } from "../profiles.ts"; export enum PhotonRegionCodeString { diff --git a/src/data/profile/avatar.ts b/src/data/profile/avatar.ts index d47a0ea..6d86593 100644 --- a/src/data/profile/avatar.ts +++ b/src/data/profile/avatar.ts @@ -18,7 +18,7 @@ along with this program. If not, see . */ import { Redis } from "../../db.ts"; import { ProfileContentManager } from "./base/profilemanagerbase.ts"; -interface AvatarSettings { +export interface AvatarSettings { OutfitSelections: string, HairColor: string, SkinColor: string, diff --git a/src/data/profile/settings.ts b/src/data/profile/settings.ts index f568d30..f8455de 100644 --- a/src/data/profile/settings.ts +++ b/src/data/profile/settings.ts @@ -37,11 +37,11 @@ export class ProfileSettingsManager extends ProfileContentManager { return await Redis.Database.hget(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings), key); } - async setSetting(key: SettingKey, value: string) { + async setSetting(key: SettingKey, value: string | number) { await Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings), key, value); } - async setSettingRaw(key: string, value: string) { + async setSettingRaw(key: string, value: string | number) { await Redis.Database.hset(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId.toString(), Redis.KeyGroups.Profiles.Settings), key, value); } diff --git a/src/data/profiles.ts b/src/data/profiles.ts index a308f31..fb37336 100644 --- a/src/data/profiles.ts +++ b/src/data/profiles.ts @@ -217,6 +217,16 @@ class Profile { return (await Redis.Database.sismember(Redis.buildKey(Redis.KeyGroups.Operators), this.#id.toString())) >= 1; } + async getBio() { + const bio = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Bio)); + if (!bio) return "This player has not yet set their bio. Remind them to set one!"; + else return bio; + } + + async setBio(bio: string) { + await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Bio), bio); + } + async export() { return await Profile.getExportAccount(this.#id); } @@ -256,7 +266,7 @@ class Profile { const data = await this.Settings.getSetting(SettingKey.VRMovementMode); if (data == null) { - log.w(`No known VR movement mode for ${this.#id} (harmless if OOBE not ran)`); + log.w(`No known VR movement mode for ${this.#id}`); return VRMovementMode.Teleport; } const parsedData = parseInt(data); diff --git a/src/db.ts b/src/db.ts index be5d084..b4c0ef5 100644 --- a/src/db.ts +++ b/src/db.ts @@ -86,6 +86,7 @@ export const KeyGroups = { Settings: "settings", DeviceClass: "deviceClass", Xp: "xp", + Bio: "bio", Relationships: { Root: "relationships", IncomingFriendRequests: "incomingFriendRequests", diff --git a/src/routes/account.ts b/src/routes/account.ts index 55d1379..665e507 100644 --- a/src/routes/account.ts +++ b/src/routes/account.ts @@ -17,7 +17,9 @@ along with this program. If not, see . */ import { APIUtils } from "../apiutils.ts"; import { route as AccountRoute } from "./account/account.ts"; +import { route as ParentalControlRoute } from "./account/parentalcontrol.ts"; export const route = APIUtils.createRouter("/accountservice"); route.router.use(AccountRoute.path, AccountRoute.router); +route.router.use(ParentalControlRoute.path, ParentalControlRoute.router); \ No newline at end of file diff --git a/src/routes/account/account.ts b/src/routes/account/account.ts index 2bd9cbf..69ddc92 100644 --- a/src/routes/account/account.ts +++ b/src/routes/account/account.ts @@ -17,17 +17,11 @@ along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; import express from "express"; -import { Profile } from "../../data/profiles.ts"; +import UnifiedProfile, { Profile } from "../../data/profiles.ts"; import { z } from "zod"; export const route = APIUtils.createRouter("/account"); -interface CreateAccountRequestBody { - platform: string; - platformId: string; - deviceId: string; -} - const CreateAccountRequestBodySchema = z.object({ platform: z.string(), platformId: z.string(), @@ -106,4 +100,33 @@ route.router.get("/me", }, +); + +interface BioFetchParams { + id?: string +} +route.router.get('/:id/bio', + + APIUtils.Authentication, + + async (rq: express.Request, rs: express.Response) => { + + const unparsedId = rq.params.id; + if (!unparsedId) { + rs.sendStatus(500); + return; + } + const parsedId = parseInt(unparsedId); + if (isNaN(parsedId)) { + rs.sendStatus(400); + return; + } + const player = UnifiedProfile.get(parsedId); + + rs.json({ + accountId: parsedId, + bio: await player.getBio(), + }); + } + ); \ No newline at end of file diff --git a/src/routes/account/parentalcontrol.ts b/src/routes/account/parentalcontrol.ts new file mode 100644 index 0000000..4a7b9be --- /dev/null +++ b/src/routes/account/parentalcontrol.ts @@ -0,0 +1,35 @@ +/* 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 { APIUtils } from "../../apiutils.ts"; +import { AuthType } from "../../data/users.ts"; + +export const route = APIUtils.createRouter('/parentalcontrol'); + +route.router.get('/me', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + (_rq, rs) => { + rs.json({ + accountId: rs.locals.profile.getId(), + disallowInAppPurchases: true // no transations here buddy + }); + }, + +); \ No newline at end of file diff --git a/src/routes/api.ts b/src/routes/api.ts index c9bff07..9890d63 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -30,6 +30,14 @@ import { route as AvatarRoute } from "./api/avatar.ts"; import { route as QuickPlayRoute } from "./api/quickplay.ts"; import { route as RoomsRoute } from "./api/rooms.ts"; import { route as ChallengeRoute } from "./api/challenge.ts"; +import { route as EquipmentRoute } from "./api/equipment.ts"; +import { route as ConsumablesRoute } from "./api/consumables.ts"; +import { route as ObjectivesRoute } from "./api/objectives.ts"; +import { route as ChecklistRoute } from "./api/checklist.ts"; +import { route as ImagesRoute } from "./api/images.ts"; +import { route as CommunityBoardRoute } from "./api/communityboard.ts"; +import { route as PlayerEventsRoute } from "./api/playerevents.ts"; +import { route as StorefrontsRoute } from "./api/storefronts.ts"; export const route = APIUtils.createRouter("/api"); @@ -46,4 +54,12 @@ route.router.use(PlayerReputationRoute.path, PlayerReputationRoute.router); route.router.use(AvatarRoute.path, AvatarRoute.router); route.router.use(QuickPlayRoute.path, QuickPlayRoute.router); route.router.use(RoomsRoute.path, RoomsRoute.router); -route.router.use(ChallengeRoute.path, ChallengeRoute.router); \ No newline at end of file +route.router.use(ChallengeRoute.path, ChallengeRoute.router); +route.router.use(EquipmentRoute.path, EquipmentRoute.router); +route.router.use(ConsumablesRoute.path, ConsumablesRoute.router); +route.router.use(ObjectivesRoute.path, ObjectivesRoute.router); +route.router.use(ChecklistRoute.path, ChecklistRoute.router); +route.router.use(ImagesRoute.path, ImagesRoute.router); +route.router.use(CommunityBoardRoute.path, CommunityBoardRoute.router); +route.router.use(PlayerEventsRoute.path, PlayerEventsRoute.router); +route.router.use(StorefrontsRoute.path, StorefrontsRoute.router); \ No newline at end of file diff --git a/src/routes/api/avatar.ts b/src/routes/api/avatar.ts index 17c2193..c673852 100644 --- a/src/routes/api/avatar.ts +++ b/src/routes/api/avatar.ts @@ -15,8 +15,11 @@ 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 { APIUtils } from "../../apiutils.ts"; +import { z } from "zod"; +import { APIUtils, NoBody } from "../../apiutils.ts"; import { AuthType } from "../../data/users.ts"; +import express from "express"; +import { AvatarSettings } from "../../data/profile/avatar.ts"; export const route = APIUtils.createRouter("/avatar"); @@ -31,13 +34,49 @@ route.router.get('/v2', ); +const AvatarSettingsSchema = z.object({ + OutfitSelections: z.string(), + HairColor: z.string(), + SkinColor: z.string(), + FaceFeatures: z.string() +}); +route.router.post('/v2/set', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + express.json(), + APIUtils.validateRequestBody(AvatarSettingsSchema), + + (rq: express.Request, rs: express.Response) => { + rs.locals.profile.Avatar.setAvatar(rq.body); + rs.sendStatus(200); + }, + +); + route.router.get('/v4/items', APIUtils.Authentication, APIUtils.AuthenticationType(AuthType.Game), - (_rq, rs) => { - rs.json([]); - } + APIUtils.emptyArrayResponse + +); + +route.router.get('/v3/saved', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + APIUtils.emptyArrayResponse + +); + +route.router.get('/v2/gifts', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + APIUtils.emptyArrayResponse ); \ No newline at end of file diff --git a/src/routes/api/checklist.ts b/src/routes/api/checklist.ts new file mode 100644 index 0000000..2ce13b4 --- /dev/null +++ b/src/routes/api/checklist.ts @@ -0,0 +1,30 @@ +/* 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 { APIUtils } from "../../apiutils.ts"; +import { AuthType } from "../../data/users.ts"; + +export const route = APIUtils.createRouter('/checklist'); + +route.router.get('/v1/current', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + APIUtils.emptyArrayResponse + +); \ No newline at end of file diff --git a/src/routes/api/communityboard.ts b/src/routes/api/communityboard.ts new file mode 100644 index 0000000..4399e75 --- /dev/null +++ b/src/routes/api/communityboard.ts @@ -0,0 +1,51 @@ +/* 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 { APIUtils } from "../../apiutils.ts"; +import { Config } from "../../config.ts"; +import { AuthType } from "../../data/users.ts"; + +const config = Config.getConfig(); + +export const route = APIUtils.createRouter('/communityboard'); + +route.router.get('/v1/current', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + (_rq, rs) => { + rs.json({ + FeaturedPlayer: { + Id: 1, + TitleOverride: "", + UrlOverride: "" + }, + FeaturedRoomGroup: { + Name: "", + FeaturedRooms: [] + }, + CurrentAnnouncement: { + Message: config.public.motd, + MoreInfoUrl: "" + }, + InstagramImages: [], + Videos: [] + }); + }, + +); \ No newline at end of file diff --git a/src/routes/api/consumables.ts b/src/routes/api/consumables.ts new file mode 100644 index 0000000..0a6c1fd --- /dev/null +++ b/src/routes/api/consumables.ts @@ -0,0 +1,30 @@ +/* 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 { APIUtils } from "../../apiutils.ts"; +import { AuthType } from "../../data/users.ts"; + +export const route = APIUtils.createRouter('/consumables'); + +route.router.get('/v1/getUnlocked', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + APIUtils.emptyArrayResponse + +); \ No newline at end of file diff --git a/src/routes/api/equipment.ts b/src/routes/api/equipment.ts new file mode 100644 index 0000000..4fed534 --- /dev/null +++ b/src/routes/api/equipment.ts @@ -0,0 +1,30 @@ +/* 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 { APIUtils } from "../../apiutils.ts"; +import { AuthType } from "../../data/users.ts"; + +export const route = APIUtils.createRouter('/equipment'); + +route.router.get('/v2/getUnlocked', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + APIUtils.emptyArrayResponse + +) \ No newline at end of file diff --git a/src/routes/api/gameconfigs.ts b/src/routes/api/gameconfigs.ts index 94e44de..6a9bdad 100644 --- a/src/routes/api/gameconfigs.ts +++ b/src/routes/api/gameconfigs.ts @@ -19,6 +19,4 @@ import { APIUtils } from "../../apiutils.ts"; export const route = APIUtils.createRouter("/gameconfigs"); -route.router.get("/v1/all", (_rq, rs) => { - rs.json([]); -}); +route.router.get("/v1/all", APIUtils.emptyArrayResponse); \ No newline at end of file diff --git a/src/routes/api/images.ts b/src/routes/api/images.ts new file mode 100644 index 0000000..c7f9527 --- /dev/null +++ b/src/routes/api/images.ts @@ -0,0 +1,30 @@ +/* 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 { APIUtils } from "../../apiutils.ts"; +import { AuthType } from "../../data/users.ts"; + +export const route = APIUtils.createRouter('/images'); + +route.router.get('/v2/named', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + APIUtils.emptyArrayResponse + +); \ No newline at end of file diff --git a/src/routes/api/messages.ts b/src/routes/api/messages.ts index d3cff2e..da8ab30 100644 --- a/src/routes/api/messages.ts +++ b/src/routes/api/messages.ts @@ -23,8 +23,6 @@ route.router.get('/v2/get', APIUtils.Authentication, - (_rq, rs) => { - rs.json([]); // temporary - } + APIUtils.emptyArrayResponse ) \ No newline at end of file diff --git a/src/routes/api/objectives.ts b/src/routes/api/objectives.ts new file mode 100644 index 0000000..017a204 --- /dev/null +++ b/src/routes/api/objectives.ts @@ -0,0 +1,35 @@ +/* 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 { APIUtils } from "../../apiutils.ts"; +import { AuthType } from "../../data/users.ts"; + +export const route = APIUtils.createRouter('/objectives'); + +route.router.get('/v1/myprogress', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + (_rq, rs) => { + rs.json({ + Objectives: [], + ObjectiveGroups: [] + }); + }, + +); \ No newline at end of file diff --git a/src/routes/api/playerevents.ts b/src/routes/api/playerevents.ts new file mode 100644 index 0000000..b36b06e --- /dev/null +++ b/src/routes/api/playerevents.ts @@ -0,0 +1,44 @@ +/* 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 { APIUtils } from "../../apiutils.ts"; +import { AuthType } from "../../data/users.ts"; + +export const route = APIUtils.createRouter('/playerevents'); + +route.router.get('/v1/all', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + (_rq, rs) => { + rs.json({ + Created: [], + Responses: [] + }); + }, + +); + +route.router.get('/v1', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + APIUtils.emptyArrayResponse + +); \ No newline at end of file diff --git a/src/routes/api/playersubscriptions.ts b/src/routes/api/playersubscriptions.ts index 08549e4..5db75f2 100644 --- a/src/routes/api/playersubscriptions.ts +++ b/src/routes/api/playersubscriptions.ts @@ -21,8 +21,6 @@ export const route = APIUtils.createRouter("/playersubscriptions"); route.router.get('/v1/my', - (_rq, rs) => { - rs.json([]); // temporary: todo - } + APIUtils.emptyArrayResponse ); \ No newline at end of file diff --git a/src/routes/api/rooms.ts b/src/routes/api/rooms.ts index d0f04e5..6150cc2 100644 --- a/src/routes/api/rooms.ts +++ b/src/routes/api/rooms.ts @@ -17,6 +17,7 @@ along with this program. If not, see . */ import { APIUtils } from "../../apiutils.ts"; import Rooms from "../../data/content/rooms.ts"; +import { RoomAccessibility } from "../../data/content/roomtypes.ts"; import { AuthType } from "../../data/users.ts"; import express from "express"; @@ -46,4 +47,75 @@ route.router.get('/v4/details/:roomId', else rs.json(room); }, -) \ No newline at end of file +); + +route.router.get('/v2/myrooms', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + APIUtils.emptyArrayResponse + +); + +route.router.get('/v1/hot', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + async (_rq, rs) => { + // temporary: return all public AG rooms for testing + const rooms = await Rooms.getAllBuiltinRoomGenerations(); + rs.json(rooms.map(room => room.Room).filter(room => room.Accessibility !== RoomAccessibility.Private)); + }, + +); + +route.router.get('/v2/baserooms', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + async (_rq, rs) => { + const rooms = await Rooms.getAllBuiltinRoomGenerations(); + rs.json(rooms.map(room => room.Room).filter(room => room.CloningAllowed)); + }, + +); + +interface GetRoomByNameParams { + name?: string +} +route.router.get('/v2/name/:name', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + async (rq: express.Request, rs: express.Response) => { + if (!rq.params.name) { + rs.sendStatus(400); + return; + } + + const room = await Rooms.getByName(rq.params.name.trim()); + if (!room || rq.params.name == 'DormRoom') { + rs.sendStatus(404); + return; + } else { + rs.json(room.Room); + return; + } + }, + +); + +route.router.post('/v1/roomRolePermissions', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + (rq, rs) => { + rs.sendStatus(200); + }, + +); \ No newline at end of file diff --git a/src/routes/api/storefronts.ts b/src/routes/api/storefronts.ts new file mode 100644 index 0000000..d6777b2 --- /dev/null +++ b/src/routes/api/storefronts.ts @@ -0,0 +1,42 @@ +/* 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 { APIUtils } from "../../apiutils.ts"; +import express from "express"; + +export const route = APIUtils.createRouter('/storefronts'); + +interface StorefrontFetchParams { + id?: string +} +route.router.get('/v3/giftdropstore/:id', + + APIUtils.Authentication, + + (rq: express.Request, rs: express.Response) => { + if (!rq.params.id) { + rs.sendStatus(400); + return; + } + rs.json({ + StorefrontType: parseInt(rq.params.id), + NextUpdate: new Date(Date.now() + 604_800_000).toISOString(), + StoreItems: [] + }); + }, + +); \ No newline at end of file diff --git a/src/routes/cdn/config.ts b/src/routes/cdn/config.ts index 94001a5..cd7ba0f 100644 --- a/src/routes/cdn/config.ts +++ b/src/routes/cdn/config.ts @@ -23,8 +23,6 @@ route.router.get('/LoadingScreenTipData', APIUtils.Authentication, - (_rq, rs) => { - rs.json([]); - } + APIUtils.emptyArrayResponse ); \ No newline at end of file diff --git a/src/routes/match.ts b/src/routes/match.ts index 43e14e5..0b42916 100644 --- a/src/routes/match.ts +++ b/src/routes/match.ts @@ -18,8 +18,10 @@ along with this program. If not, see . */ import { APIUtils } from "../apiutils.ts"; import { route as PlayerRoute } from "./match/player.ts"; import { route as GotoRoute } from "./match/goto.ts"; +import { route as RoomRoute } from "./match/room.ts"; export const route = APIUtils.createRouter('/match'); route.router.use(PlayerRoute.path, PlayerRoute.router); -route.router.use(GotoRoute.path, GotoRoute.router); \ No newline at end of file +route.router.use(GotoRoute.path, GotoRoute.router); +route.router.use(RoomRoute.path, RoomRoute.router); \ No newline at end of file diff --git a/src/routes/match/goto.ts b/src/routes/match/goto.ts index 030288c..6c8749c 100644 --- a/src/routes/match/goto.ts +++ b/src/routes/match/goto.ts @@ -15,12 +15,15 @@ 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 { APIUtils } from "../../apiutils.ts"; import Matchmaking from "../../data/live/base.ts"; import { MatchmakingErrorCode } from "../../data/live/types.ts"; import { AuthType } from "../../data/users.ts"; import express from "express"; +const log = new Logging("MatchGotoRoute"); + export const route = APIUtils.createRouter('/goto'); interface MatchmakingParams { @@ -35,6 +38,7 @@ route.router.post('/room/:roomName', async (rq: express.Request, rs: express.Response) => { if (!rq.params.roomName) { + log.d('Matchmake failed: No room specified'); rs.json({ errorCode: MatchmakingErrorCode.NoSuchRoom }); diff --git a/src/routes/match/player.ts b/src/routes/match/player.ts index 9bf929f..9df6494 100644 --- a/src/routes/match/player.ts +++ b/src/routes/match/player.ts @@ -23,6 +23,8 @@ import Presence, { PresenceExport } from "../../data/live/presence.ts"; import { AuthType } from "../../data/users.ts"; import Logging from "@proxnet/undead-logging"; import UnifiedProfile from "../../data/profiles.ts"; +import { PlayerStatusVisibility, VRMovementMode } from "../../data/live/types.ts"; +import { SettingKey } from "../../data/content/settings.ts"; const log = new Logging("MatchPlayerRoute"); @@ -51,7 +53,6 @@ route.router.get('/', const presExport: PresenceExport[] = []; for (const id of ids) { const pres = await Presence.get(UnifiedProfile.get(id)); - await pres.update(); presExport.push(await pres.export()); } @@ -100,8 +101,51 @@ route.router.post('/heartbeat', async (_rq, rs) => { const pres = await Presence.get(rs.locals.profile); - await pres.update(); rs.json(await pres.export()); } +); + +interface StatusVisibilityBody { + statusVisibility: PlayerStatusVisibility +} +const StatusVisibilitySchema = z.object({ + statusVisibility: z.nativeEnum(PlayerStatusVisibility) +}); + +route.router.put('/statusvisibility', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + express.urlencoded({ extended: true }), + APIUtils.validateRequestBody(StatusVisibilitySchema), + + async (rq: express.Request, rs: express.Response) => { + rs.locals.profile.Settings.setSetting(SettingKey.PlayerStatusVisibility, rq.body.statusVisibility.toString()); + (await Presence.get(rs.locals.profile)).updateStatusVisibility(); + rs.sendStatus(200); + }, + +); + +interface VRMovementModeBody { + vrMovementMode: PlayerStatusVisibility +} +const VRMovementModeSchema = z.object({ + vrMovementMode: z.nativeEnum(VRMovementMode) +}); + +route.router.put('/vrmovementmode', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + express.urlencoded({ extended: true }), + APIUtils.validateRequestBody(VRMovementModeSchema), + + async (rq: express.Request, rs: express.Response) => { + rs.locals.profile.Settings.setSetting(SettingKey.VRMovementMode, rq.body.vrMovementMode.toString()); + (await Presence.get(rs.locals.profile)).updateVRMovementMode(); + rs.sendStatus(200); + }, + ); \ No newline at end of file diff --git a/src/routes/match/room.ts b/src/routes/match/room.ts new file mode 100644 index 0000000..a025925 --- /dev/null +++ b/src/routes/match/room.ts @@ -0,0 +1,56 @@ +/* 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 { APIUtils } from "../../apiutils.ts"; +import Instances from "../../data/live/instances.ts"; +import { AuthType } from "../../data/users.ts"; +import express from "express"; + +export const route = APIUtils.createRouter('/room'); + +interface RoomGetInstancesParams { + id?: string +} +route.router.get('/:id/instances', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + (rq: express.Request, rs: express.Response) => { + // TODO: send forbidden request when player is requesting instances for a room that they do not have permissions for + if (!rq.params.id) { + rs.sendStatus(400); + return; + } + const parsedId = parseInt(rq.params.id); + if (isNaN(parsedId)) { + rs.sendStatus(400); + return; + } + + const instances = Instances.getAllRoomInstances(parsedId); + rs.json(instances.values().toArray().map(instance => ({ + roomInstanceId: instance.roomInstanceId, + roomId: parsedId, + subRoomId: instance.subRoomId, + isFull: instance.isFull, + createdAt: new Date().toISOString(), // TODO: rewrite instance - create instance class rather than using sets - include datetime when instance was created + playerIds: Instances.getInstancePlayers(instance).values().toArray().map(profile => profile.getId()) + }))); + }, + +); \ No newline at end of file diff --git a/src/socket/socket.ts b/src/socket/socket.ts index e190bc2..23ca2c0 100644 --- a/src/socket/socket.ts +++ b/src/socket/socket.ts @@ -21,6 +21,7 @@ import { CompletionMessage, Message, MessageKind, + PushNotificationId, SignalMessageType, SignalRMessage, SignalRMessageSchema, @@ -32,6 +33,7 @@ import { } from "./types.ts"; import { SocketTarget } from "./targets/targetbase.ts"; import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts"; +import Presence from "../data/live/presence.ts"; export class SignalRSocketHandler { @@ -42,6 +44,8 @@ export class SignalRSocketHandler { #Targets: Map = new Map(); + #PresenceUpdateId: number; + constructor(socket: WebSocket, player: Profile) { this.#socket = socket; @@ -53,6 +57,11 @@ export class SignalRSocketHandler { this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget()); + this.#PresenceUpdateId = setInterval(async () => { + const pres = await Presence.get(this.#profile); + this.sendNotification("PresenceUpdate", await pres.export()); + }, 8000); + } async #dispatchTarget(target: string, args: unknown): Promise { @@ -136,6 +145,7 @@ export class SignalRSocketHandler { destroy(sock: SignalRSocketHandler) { return () => { + clearInterval(sock.#PresenceUpdateId); sock.sendRaw({ type: 7, error: "Socket closed" }); sock.#socket.close(); sock.#log.i(`Closed hub socket`); @@ -149,4 +159,16 @@ export class SignalRSocketHandler { this.#log.d(`SERVER MESSAGE\n ${type}\n ${JSON.stringify(data as SignalRMessage)}`); } + sendNotification(id: PushNotificationId | string, args: object) { + const msg: SignalRMessage = { + type: SignalMessageType.Invocation, + target: "Notification", + arguments: [JSON.stringify({ + Id: id, + Msg: args + })] + } + this.sendRaw(msg); + } + } \ No newline at end of file diff --git a/src/socket/types.ts b/src/socket/types.ts index 9684d3b..20e2429 100644 --- a/src/socket/types.ts +++ b/src/socket/types.ts @@ -144,4 +144,42 @@ export interface TargetResultFailure extends TargetResultBase { export interface TargetResultNotATarget extends TargetResultBase { type: TargetResultType.NotATarget } -export type TargetResult = TargetResultSuccess | TargetResultFailure | TargetResultNotATarget; \ No newline at end of file +export type TargetResult = TargetResultSuccess | TargetResultFailure | TargetResultNotATarget; + +export enum PushNotificationId { + RelationshipChanged = 1, + MessageReceived, + MessageDeleted, + PresenceHeartbeatResponse, + RefreshLogin, + Logout, + SubscriptionUpdateProfile = 11, + SubscriptionUpdatePresence, + SubscriptionUpdateGameSession, + SubscriptionUpdateRoom = 15, + ModerationQuitGame = 20, + ModerationUpdateRequired, + ModerationKick, + ModerationKickAttemptFailed, + ModerationRoomBan, + ServerMaintenance, + GiftPackageReceived = 30, + GiftPackageReceivedImmediate, + ProfileJuniorStatusUpdate = 40, + RelationshipsInvalid = 50, + StorefrontBalanceAdd = 60, + StorefrontBalanceUpdate, + StorefrontBalancePurchase, + ConsumableMappingAdded = 70, + ConsumableMappingRemoved, + PlayerEventCreated = 80, + PlayerEventUpdated, + PlayerEventDeleted, + PlayerEventResponseChanged, + PlayerEventResponseDeleted, + PlayerEventStateChanged, + ChatMessageReceived = 90, + CommunityBoardUpdate = 95, + CommunityBoardAnnouncementUpdate, + InventionModerationStateChanged = 100 +}