From 026f9c8bd8156d90a9e185437ce13548183193ed Mon Sep 17 00:00:00 2001 From: zombieb Date: Sat, 29 Mar 2025 23:09:40 -0400 Subject: [PATCH] Further the login process * Matchmaking login locks (created and checked only in memory for now) * Profile reputation temporary implementation * Profiles now no longer initialize if a user with the same username is found * vrMovementMode in presence is now required, falls back to 'Teleport' * Progression implementation began * API routes: Settings, player subscriptions, reputation, progression * cropSquare in image query is not a boolean, rather a number representing a boolean * Hile reporting uses forms, not json * Presence heartbeat and logout * Socket changes: Close event listener (destroy), send message function, targets further started --- src/apiutils.ts | 16 ++++ src/data/live/base.ts | 6 +- src/data/live/presence.ts | 6 +- src/data/profile/base/events.ts | 5 ++ .../profile/{ => base}/profilemanagerbase.ts | 0 src/data/profile/progression.ts | 82 ++++++++++++++----- src/data/profile/reputation.ts | 20 +++++ src/data/profile/settings.ts | 2 +- src/data/profiles.ts | 33 +++++++- src/db.ts | 1 + src/main.ts | 1 - src/routes/api.ts | 12 ++- src/routes/api/PlayerReporting.ts | 2 +- src/routes/api/playerReputation.ts | 24 ++++++ src/routes/api/players.ts | 27 ++++-- src/routes/api/playersubscriptions.ts | 11 +++ src/routes/api/settings.ts | 22 +++++ src/routes/img.ts | 10 ++- src/routes/match/player.ts | 34 +++++++- src/routes/user.ts | 10 +-- src/socket/socket.ts | 18 +++- src/socket/types.ts | 8 +- 22 files changed, 294 insertions(+), 56 deletions(-) create mode 100644 src/data/profile/base/events.ts rename src/data/profile/{ => base}/profilemanagerbase.ts (100%) create mode 100644 src/data/profile/reputation.ts create mode 100644 src/routes/api/playerReputation.ts create mode 100644 src/routes/api/playersubscriptions.ts create mode 100644 src/routes/api/settings.ts diff --git a/src/apiutils.ts b/src/apiutils.ts index c2fcd39..cb1aa8e 100644 --- a/src/apiutils.ts +++ b/src/apiutils.ts @@ -6,6 +6,7 @@ import { Config } from "./config.ts"; import { AuthType, User, UserTokenFormat } from "./data/users.ts"; import UnifiedProfile, { ProfileTokenFormat } from "./data/profiles.ts"; import z from "zod"; +import Matchmaking from "./data/live/base.ts"; const config = Config.getConfig(); @@ -311,6 +312,21 @@ export function AuthenticationType(type: AuthType) { } } +export function LoginLock(rq: express.Request, rs: express.Response, nxt: express.NextFunction) { + log.d(`LoginLock for ${rs.locals.profile.getId()}: ${rq.body.LoginLock}`); + const matches = Matchmaking.lockMatches(rs.locals.profile, rq.body.LoginLock); + if (matches == null) { + rs.json(genericResponseFormat(true, "Login Lock failure")); + return; + } else { + if (matches) nxt(); + else { + rs.json(genericResponseFormat(true, "Login Lock failure")); + return; + } + } +} + export type NoBody = Record; export * as APIUtils from "./apiutils.ts"; diff --git a/src/data/live/base.ts b/src/data/live/base.ts index 76550e5..da63ce0 100644 --- a/src/data/live/base.ts +++ b/src/data/live/base.ts @@ -10,9 +10,9 @@ class MatchmakingBase { } lockMatches(prof: Profile, lock: string) { - const maybeLock = loginLocks.get(prof.getId()); - if (maybeLock) return maybeLock == lock; - else return false; + const checkLock = loginLocks.get(prof.getId()); + if (checkLock) return checkLock == lock; + else return null; } deleteLoginLock(prof: Profile) { diff --git a/src/data/live/presence.ts b/src/data/live/presence.ts index 4b3be30..c3012a1 100644 --- a/src/data/live/presence.ts +++ b/src/data/live/presence.ts @@ -46,7 +46,7 @@ class PlayerPresence { playerId: number; statusVisibility: PlayerStatusVisibility; deviceClass: DeviceClass; - vrMovementMode: VRMovementMode | undefined; + vrMovementMode: VRMovementMode; roomInstance: RoomInstance | null; lastSeen: Date; @@ -85,9 +85,9 @@ class PlayerPresence { roomInstance: this.roomInstance, statusVisibility: this.statusVisibility, deviceClass: this.deviceClass, - vrMovementMode: this.vrMovementMode ? this.vrMovementMode : undefined + vrMovementMode: this.vrMovementMode } - return Object.assign({}, exp); // hard copy/clone + return Object.assign({}, exp); // hard clone } updateOffline() { diff --git a/src/data/profile/base/events.ts b/src/data/profile/base/events.ts new file mode 100644 index 0000000..d3a13a5 --- /dev/null +++ b/src/data/profile/base/events.ts @@ -0,0 +1,5 @@ +export class ProfileEventsManager { + + + +} \ No newline at end of file diff --git a/src/data/profile/profilemanagerbase.ts b/src/data/profile/base/profilemanagerbase.ts similarity index 100% rename from src/data/profile/profilemanagerbase.ts rename to src/data/profile/base/profilemanagerbase.ts diff --git a/src/data/profile/progression.ts b/src/data/profile/progression.ts index 8612f4f..efcb8ae 100644 --- a/src/data/profile/progression.ts +++ b/src/data/profile/progression.ts @@ -1,35 +1,79 @@ +import Logging from "@proxnet/undead-logging"; import { Config } from "../../config.ts"; import { GameConfigs } from "../config.ts"; -import { ProfileContentManager } from "./profilemanagerbase.ts"; +import { ProfileContentManager } from "./base/profilemanagerbase.ts"; +import { Redis } from "../../db.ts"; + +const log = new Logging("ProfileProgression"); -const serverConfig = Config.getConfig(); const config = GameConfigs.getConfig(); -/** - * Level -> Required XP - */ -const requiredXpMap: Map = new Map(); export class ProfileProgressionManager extends ProfileContentManager { - constructor() { - super(); - // fill `requiredXpMap` using `config.public` values - } + async #getNextLevelRequiredXp() { + const xp = await this.getXp(); + if (typeof config?.LevelProgressionMaps == 'undefined') return null; + for (const item of config?.LevelProgressionMaps) { + if (xp >= item.RequiredXp) { - #getRequiredXp(level: number) { - if (level > serverConfig.public.maxLevels) return null; - else { - const req = requiredXpMap.get(level); - return req ? req : null; + const next = config?.LevelProgressionMaps[config?.LevelProgressionMaps.indexOf(item) + 1]; + if (typeof next == 'undefined') return null; + else return next.RequiredXp; + + } } } - getLevel() { - return 30; // temporary + /** + * Set the profile's exact # of XP + * @returns The new # of XP + */ + async setXp(xp: number) { + await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId!.toString(), Redis.KeyGroups.Profiles.Xp), xp.toString()); + return xp; } - getXp() { - return 0; // temporary + /** + * Adds an integer to the profile's # of XP + * @returns The new # of XP; returns 0 if data is in error + */ + async addXp(xp: number) { + const currentXp = await this.getXp(); + if (currentXp == null) return 0; + await this.setXp(currentXp + xp); + return currentXp + xp; + } + + /** + * Get the player's current level + * @returns The player's level based on their current XP; returns 1 if data is in error + */ + async getLevel() { + const xp = await this.getXp(); + if (xp == null) return 1; // fallback since progression data is required + + if (typeof config?.LevelProgressionMaps == 'undefined') return null; + for (const item of config?.LevelProgressionMaps) { + if (xp >= item.RequiredXp) { + + const current = config?.LevelProgressionMaps[config?.LevelProgressionMaps.indexOf(item)]; + if (typeof current == 'undefined') return null; + else return current.Level; + + } + } + } + + async getXp() { + if (!this.profileIsSet()) throw this.profileNotSetError; + let data = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.profileId!.toString(), Redis.KeyGroups.Profiles.Xp)); + if (data == null) data = (await this.setXp(0)).toString(); + + const parsedData = parseInt(data); + if (isNaN(parsedData)) { + log.w(`Parsed xp data for ${this.profileId} is NaN!`); + return 0; // fallback since progression data is required + } else return parsedData; } } \ No newline at end of file diff --git a/src/data/profile/reputation.ts b/src/data/profile/reputation.ts new file mode 100644 index 0000000..9fea0fc --- /dev/null +++ b/src/data/profile/reputation.ts @@ -0,0 +1,20 @@ +import { ProfileContentManager } from "./base/profilemanagerbase.ts"; + +export class ProfileReputationManager extends ProfileContentManager { + + async getReputation() { // async temporary + return { + AccountId: this.profileId, + Noterity: 0.0, + CheerGeneral: 0, + CheerHelpful: 0, + CheerSportsman: 0, + CheerCreative: 0, + CheerCredit: 0, + SubscriberCount: 0, + SubscribedCount: 0, + SelectedCheer: 0 + }; + } + +} \ No newline at end of file diff --git a/src/data/profile/settings.ts b/src/data/profile/settings.ts index 27004e1..91eaeb7 100644 --- a/src/data/profile/settings.ts +++ b/src/data/profile/settings.ts @@ -1,6 +1,6 @@ import { Redis } from "../../db.ts"; import { SettingKey } from "../content/settings.ts"; -import { ProfileContentManager } from "./profilemanagerbase.ts"; +import { ProfileContentManager } from "./base/profilemanagerbase.ts"; export interface Setting { Key: string; diff --git a/src/data/profiles.ts b/src/data/profiles.ts index 19d8d4b..4dbe4a3 100644 --- a/src/data/profiles.ts +++ b/src/data/profiles.ts @@ -10,9 +10,13 @@ import { z } from "zod"; import { SignalRSocketHandler } from "../socket/socket.ts"; import { ProfileSettingsManager } from "./profile/settings.ts"; import { ProfileProgressionManager } from "./profile/progression.ts"; +import { ProfileReputationManager } from "./profile/reputation.ts"; +import Logging from "@proxnet/undead-logging"; const config = Config.getConfig(); +const log = new Logging("Profiles"); + interface ProfileInitOptions { username: string; } @@ -79,6 +83,11 @@ class Profile { static async init(options?: ProfileInitOptions) { const optionsSpecified = typeof options !== "undefined"; + if (options?.username) { + const existingUser = await Profile.byName(options.username); + if (existingUser == null) return null; + } + const newId = await this.getUniqueId(); const newUsername = optionsSpecified ? options.username @@ -143,6 +152,7 @@ class Profile { Settings = new ProfileSettingsManager(); Progression = new ProfileProgressionManager(); + Reputation = new ProfileReputationManager(); constructor(id: number) { this.#id = id; @@ -150,6 +160,7 @@ class Profile { // Set IDs for all content managers this.Settings.setProfile(this.#id); this.Progression.setProfile(this.#id); + this.Reputation.setProfile(this.#id); } setInstance(instance: RoomInstance | null) { @@ -178,11 +189,20 @@ class Profile { async getKnownDeviceClass() { const data = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DeviceClass)); + if (data == null) { + log.w(`No known device class for ${this.#id}`); + return DeviceClass.Unknown; + } + const parsedData = parseInt(data); + if (isNaN(parsedData)) { + log.w(`Malformed device class for ${this.#id}`); + return DeviceClass.Unknown; + } const DeviceClassEnum = z.nativeEnum(DeviceClass); type DeviceClassEnum = z.infer - const result = DeviceClassEnum.safeParse(data); + const result = DeviceClassEnum.safeParse(parsedData); if (result.success) return result.data; else return DeviceClass.Unknown; } @@ -193,11 +213,20 @@ class Profile { async getVRMovementMode() { const data = await this.Settings.getSetting(SettingKey.VRMovementMode); + if (data == null) { + log.w(`No known device class for ${this.#id}`); + return VRMovementMode.Teleport; + } + const parsedData = parseInt(data); + if (isNaN(parsedData)) { + log.w(`Malformed device class for ${this.#id}`); + return VRMovementMode.Teleport; + } const VRMovementModeEnum = z.nativeEnum(VRMovementMode); type VRMovementModeEnum = z.infer - const result = VRMovementModeEnum.safeParse(data); + const result = VRMovementModeEnum.safeParse(parsedData); if (result.success) return result.data; else return VRMovementMode.Teleport; } diff --git a/src/db.ts b/src/db.ts index 15de49e..ad40056 100644 --- a/src/db.ts +++ b/src/db.ts @@ -67,6 +67,7 @@ export const KeyGroups = { DisplayName: "displayName", Settings: "settings", DeviceClass: "deviceClass", + Xp: "xp", }, Operators: "operators", Users: { diff --git a/src/main.ts b/src/main.ts index cc70f34..a90fba9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -137,7 +137,6 @@ try { if (path === '/negotiate' && req.method == 'POST') return new Response(JSON.stringify({})); - if (!upgrade) return new Response(null, { status: 401 }); const authResult = await authenticate(req); diff --git a/src/routes/api.ts b/src/routes/api.ts index 6a6bfc8..8caf681 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -1,10 +1,14 @@ +import { APIUtils } from "../apiutils.ts"; 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 { route as MessagesRoute } from "./api/messages.ts"; import { route as RelationshipsRoute } from "./api/relationships.ts"; -import { APIUtils } from "../apiutils.ts"; +import { route as PlayersRoute } from "./api/players.ts" +import { route as SettingsRoute } from "./api/settings.ts"; +import { route as PlayerSubscriptionsRoute } from "./api/playersubscriptions.ts"; +import { route as PlayerReputationRoute } from "./api/playerReputation.ts"; export const route = APIUtils.createRouter("/api"); @@ -13,4 +17,8 @@ route.router.use(ConfigRoute.path, ConfigRoute.router); route.router.use(GameConfig.path, GameConfig.router); route.router.use(PlayerReportingRoute.path, PlayerReportingRoute.router); route.router.use(MessagesRoute.path, MessagesRoute.router); -route.router.use(RelationshipsRoute.path, RelationshipsRoute.router); \ No newline at end of file +route.router.use(RelationshipsRoute.path, RelationshipsRoute.router); +route.router.use(PlayersRoute.path, PlayersRoute.router); +route.router.use(SettingsRoute.path, SettingsRoute.router); +route.router.use(PlayerSubscriptionsRoute.path, PlayerSubscriptionsRoute.router); +route.router.use(PlayerReputationRoute.path, PlayerReputationRoute.router); \ No newline at end of file diff --git a/src/routes/api/PlayerReporting.ts b/src/routes/api/PlayerReporting.ts index c60de69..26e38e6 100644 --- a/src/routes/api/PlayerReporting.ts +++ b/src/routes/api/PlayerReporting.ts @@ -18,7 +18,7 @@ const HileMessageSchema = z.object({ route.router.post('/v1/hile', APIUtils.Authentication, - express.json(), + express.urlencoded({ extended: true }), APIUtils.validateRequestBody(HileMessageSchema), (rq: express.Request, rs) => { diff --git a/src/routes/api/playerReputation.ts b/src/routes/api/playerReputation.ts new file mode 100644 index 0000000..a1ad0f0 --- /dev/null +++ b/src/routes/api/playerReputation.ts @@ -0,0 +1,24 @@ +import { APIUtils } from "../../apiutils.ts"; +import UnifiedProfile from "../../data/profiles.ts"; +import { AuthType } from "../../data/users.ts"; +import express from "express"; + +export const route = APIUtils.createRouter("/playerReputation"); + +route.router.get('/v1/:id', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + (rq: express.Request<{ id: string }>, rs) => { + const unparsedPlayerId = rq.params.id; + const parsedPlayerId = parseInt(unparsedPlayerId); + if (isNaN(parsedPlayerId)) { + rs.json(APIUtils.genericResponseFormat(true, 'The player ID was invalid.')); + return; + } + + rs.json(UnifiedProfile.get(parsedPlayerId).Reputation.getReputation()); + } + +); \ No newline at end of file diff --git a/src/routes/api/players.ts b/src/routes/api/players.ts index 4392524..36a649c 100644 --- a/src/routes/api/players.ts +++ b/src/routes/api/players.ts @@ -1,4 +1,9 @@ +import Logging from "@proxnet/undead-logging"; import { APIUtils } from "../../apiutils.ts"; +import express from "express"; +import UnifiedProfile from "../../data/profiles.ts"; + +const log = new Logging("ProgressionRoute"); export const route = APIUtils.createRouter("/players"); @@ -6,12 +11,22 @@ route.router.get('/v1/progression/:id', APIUtils.Authentication, - async (_rq, rs) => { - rs.json({ - PlayerId: rs.locals.profile.getId(), - Level: await rs.locals.profile.Progression.getLevel(), // await is temporary - Xp: await rs.locals.profile.Progression.getXp() - }); + async (rq: express.Request<{ id: string }>, rs) => { + const unparsedPlayerId = rq.params.id; + const parsedPlayerId = parseInt(unparsedPlayerId); + if (isNaN(parsedPlayerId)) { + rs.json(APIUtils.genericResponseFormat(true, 'The player ID was invalid.')); + return; + } + + const profile = UnifiedProfile.get(parsedPlayerId); + const res = { + PlayerId: profile.getId(), + Level: await profile.Progression.getLevel(), + XP: await profile.Progression.getXp() + }; + log.d(`prog res: ${JSON.stringify(res)}`); + rs.json(res); } ); \ No newline at end of file diff --git a/src/routes/api/playersubscriptions.ts b/src/routes/api/playersubscriptions.ts new file mode 100644 index 0000000..2512c38 --- /dev/null +++ b/src/routes/api/playersubscriptions.ts @@ -0,0 +1,11 @@ +import { APIUtils } from "../../apiutils.ts"; + +export const route = APIUtils.createRouter("/playersubscriptions"); + +route.router.get('/v1/my', + + (_rq, rs) => { + rs.json([]); // temporary: todo + } + +); \ No newline at end of file diff --git a/src/routes/api/settings.ts b/src/routes/api/settings.ts new file mode 100644 index 0000000..90df3e9 --- /dev/null +++ b/src/routes/api/settings.ts @@ -0,0 +1,22 @@ +import Logging from "@proxnet/undead-logging"; +import { APIUtils } from "../../apiutils.ts"; +import { AuthType } from "../../data/users.ts"; + +const log = new Logging("SettingsRoute"); + +export const route = APIUtils.createRouter("/settings"); + +route.router.get('/v2', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + + async (_rq, rs) => { + + const settings = await rs.locals.profile.Settings.getSettings(); + log.d(`settings res: ${JSON.stringify(settings)}`); + rs.json(settings); + + } + +); \ No newline at end of file diff --git a/src/routes/img.ts b/src/routes/img.ts index 1267ef9..8232943 100644 --- a/src/routes/img.ts +++ b/src/routes/img.ts @@ -44,10 +44,11 @@ route.router.get( } image = await Image.decode(imageSource); - let cropSquare: boolean = false; + let cropSquare: number | null = null; if (typeof rq.query.cropSquare == "string") { - const d = JSON.parse(rq.query.cropSquare); - if (typeof d == "boolean" && d) cropSquare = true; + const num = parseInt(rq.query.cropSquare); + if (isNaN(num)) cropSquare = null; + else cropSquare = num; } let width: number | null = null; if (typeof rq.query.width == "string") { @@ -84,7 +85,7 @@ route.router.get( } } else if (width) image.resize(width, Image.RESIZE_AUTO); else if (height) image.resize(Image.RESIZE_AUTO, height); - if (cropSquare) { + if (cropSquare == 1) { if (image.width > image.height) { image.crop( Math.round(image.width / 2) - Math.round(image.height / 2), @@ -100,6 +101,7 @@ route.router.get( );} } + rs.setHeader('content-signature', 'key-id=KEY:RSA:p1.rec.net; data=aGk='); // enable image signature patch on client rs.type("png").send(Buffer.from(await image.encode())); }, ); diff --git a/src/routes/match/player.ts b/src/routes/match/player.ts index 72923a6..232cc94 100644 --- a/src/routes/match/player.ts +++ b/src/routes/match/player.ts @@ -4,6 +4,9 @@ import express from "express"; import Matchmaking from "../../data/live/base.ts"; import Presence from "../../data/live/presence.ts"; import { AuthType } from "../../data/users.ts"; +import Logging from "@proxnet/undead-logging"; + +const log = new Logging("MatchPlayerRoute"); export const route = APIUtils.createRouter('/player'); @@ -28,4 +31,33 @@ route.router.post('/login', rs.sendStatus(200); }, -) \ No newline at end of file +); + +route.router.post('/logout', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + express.urlencoded({extended: true}), + APIUtils.validateRequestBody(LoginSchema), + + (rq, rs) => { + Matchmaking.deleteLoginLock(rs.locals.profile); + } + +) + +route.router.post('/heartbeat', + + APIUtils.Authentication, + APIUtils.AuthenticationType(AuthType.Game), + express.urlencoded({extended: true}), + APIUtils.validateRequestBody(LoginSchema), + APIUtils.LoginLock, + + async (_rq, rs) => { + const pres = await Presence.get(rs.locals.profile); + log.d(`pres heartbeat for ${rs.locals.profile.getId()}: ${JSON.stringify(await pres.export())}`); + rs.json(await pres.export()); + } + +); \ No newline at end of file diff --git a/src/routes/user.ts b/src/routes/user.ts index 20af765..56f6666 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -40,7 +40,7 @@ const AuthRequestRootSchema = z.object({ pubkey: z.string(), }); -const rateLimit = new APIUtils.RateLimiter(60, 1); +const rateLimit = new APIUtils.RateLimiter(60, 2); route.router.post("/auth", @@ -57,9 +57,7 @@ route.router.post("/auth", } if (rq.body.message.server_id !== config.public.serverId) { - log.w( - `Auth request failed (serverId mismatch), config error?\n given ID: '${rq.body.message.server_id}'\n our ID: '${config.public.serverId}'`, - ); + log.w(`Auth request failed (serverId mismatch), config error?\n given ID: '${rq.body.message.server_id}'\n our ID: '${config.public.serverId}'`); authFailed("Authentication request not intended for this server."); return; } @@ -114,9 +112,7 @@ route.router.post("/auth", } else user = obj; } if (!(await user.addNonce(rq.body.message.nonce))) { - log.w( - `Client '${rq.body.client_id}' has already used nonce. Replay attack?`, - ); + log.w(`Client '${rq.body.client_id}' has already used nonce. Replay attack?`); authFailed("Authentication request failed."); return; } diff --git a/src/socket/socket.ts b/src/socket/socket.ts index c0b7800..9b187d2 100644 --- a/src/socket/socket.ts +++ b/src/socket/socket.ts @@ -34,17 +34,17 @@ export class SignalRSocketHandler { #onMessage(message: Message) { if (message.kind == MessageKind.Protocol) { - this.#send({}); + this.sendRaw({}); return; } else { - this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type - 1]})\n ${JSON.stringify(message.data)}`); + this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type]})\n ${JSON.stringify(message.data)}`); } } async #init() { this.#log.source += this.#profile.getId().toString(); - this.#log.i(`Player '${(await this.#profile.export())?.username}' (${this.#profile.getId()}) created hub socket`); + this.#log.i(`Created hub socket`); this.#socket.addEventListener('message', message => { try { @@ -68,9 +68,19 @@ export class SignalRSocketHandler { this.#log.e(`Socket error: ${err}`); } }); + + this.#socket.addEventListener('close', this.destroy(this)); } - #send(data: object) { + destroy(sock: SignalRSocketHandler) { + return () => { + sock.sendRaw({ type: 7, error: "Socket closed by server" }); + sock.#socket.close(); + sock.#log.i(`Closed hub socket`); + } + } + + sendRaw(data: object) { this.#socket.send(`${JSON.stringify(data)}\u001e`); } diff --git a/src/socket/types.ts b/src/socket/types.ts index c25a5f3..471ee90 100644 --- a/src/socket/types.ts +++ b/src/socket/types.ts @@ -110,7 +110,8 @@ export const SignalRMessageSchema = z.discriminatedUnion("type", [ export enum TargetResultType { Success, - Failure + Failure, + NotATarget } interface TargetResultBase { type: TargetResultType @@ -122,4 +123,7 @@ export interface TargetResultSuccess extends TargetResultBase { export interface TargetResultFailure extends TargetResultBase { type: TargetResultType.Failure } -export type TargetResult = TargetResultSuccess | TargetResultFailure; \ No newline at end of file +export interface TargetResultNotATarget extends TargetResultBase { + type: TargetResultType.NotATarget +} +export type TargetResult = TargetResultSuccess | TargetResultFailure | TargetResultNotATarget; \ No newline at end of file