my power's back
All checks were successful
Galvanic Corrosion Cross-Compile / build (push) Successful in 1m57s

* Steam authentication
* Profile events
* Objective fixes for some rooms (reccenter, goldentrophy, etc)
* Profile meta (displayname) somewhat works
* Player socket subscriptions
This commit is contained in:
2025-05-04 02:25:09 -04:00
parent fe29602f4a
commit 9e4bfc8368
23 changed files with 400 additions and 79 deletions

View File

@@ -365,7 +365,7 @@ export function startTimer(_rq: express.Request, rs: express.Response, nxt: expr
nxt();
}
export function stopTimer(_rq: express.Request, rs: express.Response) {
log.n(`(${rs.locals.reqId.substring(0, 11)}) Middleware took ${(performance.now() - rs.locals.timer).toString().substring(0, 6)} ms`);
log.n(`(${rs.locals.reqId}) Middleware took ${(performance.now() - rs.locals.timer).toString().substring(0, 6)} ms`);
}
export * as APIUtils from "./apiutils.ts";

View File

@@ -71,6 +71,7 @@ type AuthConfiguration = {
* In Hours
*/
timeout: number;
steamkey: string | null;
};
export type GalvanicConfiguration = {
@@ -124,6 +125,7 @@ export const defaultConfig: GalvanicConfiguration = {
auth: {
secret: "CHANGE-ME-PLEASE",
timeout: 3,
steamkey: null
},
};

65
src/data/baseevent.ts Normal file
View File

@@ -0,0 +1,65 @@
/* Galvanic Corrosion - Rec Room custom server for communities.
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
import Logging from "@proxnet/undead-logging";
const log = new Logging("BaseEvent");
export interface Event {
time: Date
}
export class EventManager {
private eventCallbacks: Map<string, Set<(ev: unknown) => void>> = new Map();
private getSubSet(event: string): Set<(ev: unknown) => void> {
let subset = this.eventCallbacks.get(event);
if (!subset) {
subset = new Set();
this.eventCallbacks.set(event, subset);
}
return subset;
}
on<T extends Event>(event: string, cb: (ev: T) => void) {
const typeSafeCallback = ((ev: unknown) => {
cb(ev as T);
});
this.getSubSet(event).add(typeSafeCallback);
return typeSafeCallback;
}
off(event: string, cb: (ev: unknown) => void) {
const subset = this.getSubSet(event);
subset.delete(cb);
}
emit<T extends Event>(event: string, ev: T) {
const subset = this.getSubSet(event);
for (const cb of subset.values()) {
try {
cb(ev);
} catch (err) {
if (err instanceof Error) log.e(`Error when executing callback: ${err.stack}`);
else log.e(`Error when executing callback: ${err}`);
}
}
}
}

View File

@@ -67,7 +67,7 @@ export function getConfig() {
StartsInMinutes: 0,
},
LevelProgressionMaps: generateLevelProgressionMap(),
DailyObjectives: [],
DailyObjectives: [[{type: -1,score:0},{type: -1,score:0},{type: -1,score:0}],[{type: -1,score:0},{type: -1,score:0},{type: -1,score:0}],[{type: -1,score:0},{type: -1,score:0},{type: -1,score:0}]],
AutoMicMutingConfig: {
MicSpamVolumeThreshold: 1.125,
MicVolumeSampleInterval: 0.25,

94
src/data/content/data.ts Normal file
View File

@@ -0,0 +1,94 @@
/* Galvanic Corrosion - Rec Room custom server for communities.
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { Buffer } from "node:buffer";
import { Redis } from "../../db.ts";
import { generateRandomString } from "../../apiutils.ts";
export enum FileType {
Unknown,
RoomSave,
Holotar,
Image,
Video,
Invention
}
export function getFileName(prefix: string, type: FileType) {
switch (type) {
case FileType.RoomSave:
return `${prefix}.room`;
case FileType.Holotar:
return `${prefix}.holotar`;
case FileType.Image:
return `${prefix}.image`;
case FileType.Video:
return `${prefix}.video`;
case FileType.Invention:
return `${prefix}.invention`;
default:
return `${prefix}.unknown`
}
}
export async function getFile(name: string) {
const data = await Redis.Database.getBuffer(Redis.buildKey(
Redis.KeyGroups.Content.Root,
name
));
if (!data) return null;
else return data;
}
/**
* @returns Name of the new file
*/
export async function setFile(data: Buffer<ArrayBufferLike>, type: FileType, name?: string) {
let filename = generateRandomString(24);
if (name) filename = name;
const finalName = getFileName(filename, type);
await Redis.Database.set(Redis.buildKey(
Redis.KeyGroups.Content.Root,
finalName
), data);
await initFileMeta(filename, type);
return filename;
}
interface FileMeta {
created: Date | string;
fetchCount: number;
type: FileType
}
async function initFileMeta(filename: string, type: FileType) {
const meta: FileMeta = {
created: new Date(),
fetchCount: 0,
type: type
}
await Redis.Database.hset(Redis.buildKey(
Redis.KeyGroups.Content.Files,
getFileName(filename, type)
), meta);
}
export async function incrementFileFetches(name: string, type: FileType) {
}

View File

@@ -20,6 +20,7 @@ import UnifiedProfile, { Profile } from "../profiles.ts";
import { RoomInstance, InstanceOptions } from "./types.ts";
import { Config } from "../../config.ts";
import Presence from "./presence.ts";
import { RoomFactory } from "../content/rooms/RoomFactory.ts";
const log = new Logging("Instances");
@@ -177,6 +178,9 @@ class InstancesBase {
this.updateSingleInstanceIsFull(instance);
} else log.w(`Instance ${instance.roomInstanceId} is full. Cannot add player ${player.getId()}`);
const room = await new RoomFactory({ id: instance.roomId }).init();
await room?.addVisit();
}
playerIsInInstance(player: Profile, instance: RoomInstance) {

View File

@@ -19,6 +19,7 @@ import { ProfileContentManager } from "./base/profilemanagerbase.ts";
export class ProfileReputationManager extends ProfileContentManager {
// deno-lint-ignore require-await
async getReputation() { // async temporary
return {
AccountId: this.profileId,

View File

@@ -15,27 +15,13 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { Buffer } from "node:buffer";
import { Redis } from "../../db.ts";
import { Event } from "./baseevent.ts";
import { Profile } from "./profiles.ts";
export async function getImage(filename: string) {
const data = await Redis.Database.get(
Redis.buildKey(
Redis.KeyGroups.Content.Root,
Redis.KeyGroups.Content.Images,
filename,
),
);
if (data == null) return null;
else return Buffer.from(data);
}
export async function setImage(filename: string, data: Uint8Array) {
await Redis.Database.set(
Redis.buildKey(
Redis.KeyGroups.Content.Root,
Redis.KeyGroups.Content.Images,
filename,
),
Buffer.from(data),
);
export enum ProfileEvents {
BaseUpdated = "profile.updated"
}
export interface ProfileUpdatedEvent extends Event {
profile: Profile
}

View File

@@ -31,6 +31,8 @@ import { ProfileReputationManager } from "./profile/reputation.ts";
import Logging from "@proxnet/undead-logging";
import { ProfileRelationshipManager } from "./profile/relationships.ts";
import { ProfileAvatarManager } from "./profile/avatar.ts";
import { EventManager } from "./baseevent.ts";
import { ProfileEvents, ProfileUpdatedEvent } from "./profileevents.ts";
const config = Config.getConfig();
@@ -53,7 +55,7 @@ interface AccountExport {
username: string;
displayName: string;
}
interface SelfAccountExport extends AccountExport {
export interface SelfAccountExport extends AccountExport {
email?: string,
phone?: string,
juniorState?: number,
@@ -67,7 +69,7 @@ export interface ProfileTokenFormat extends TokenBaseFormat {
const reservedIds = [1, 2];
class Profile {
class Profile extends EventManager {
static async exists(id: number) {
return (await Redis.Database.exists(
Redis.buildKey(
@@ -167,8 +169,7 @@ class Profile {
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],
email: "notanemail@notanemailsite.local" // we are confirmed
displayName: values[3] == null ? (values[2] == null ? "DATABASEERROR" : values[2]) : values[3]
});
});
}
@@ -196,6 +197,8 @@ class Profile {
Avatar: ProfileAvatarManager;
constructor(id: number) {
super();
this.#id = id;
this.Settings = new ProfileSettingsManager(this.#id);
@@ -205,6 +208,14 @@ class Profile {
this.Avatar = new ProfileAvatarManager(this.#id);
}
#emitProfileUpdated() {
const ev: ProfileUpdatedEvent = {
time: new Date(),
profile: this
}
this.emit(ProfileEvents.BaseUpdated, ev);
}
setInstance(instance: RoomInstance | null) {
this.#instance = instance;
}
@@ -229,6 +240,21 @@ class Profile {
async setBio(bio: string) {
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Bio), bio);
this.#emitProfileUpdated();
}
async getDisplayName() {
const name = await Redis.Database.get(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DisplayName));
if (!name) return "DATABASE ERROR";
else return name;
}
async setDisplayName(name: string) {
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.DisplayName), name);
this.#emitProfileUpdated();
const acc = await this.export();
if (acc) this.getSocketHandler()?.sendNotification("AccountUpdate", acc);
}
async export() {

68
src/data/steam.ts Normal file
View File

@@ -0,0 +1,68 @@
/* Galvanic Corrosion - Rec Room custom server for communities.
<https://gitea.proxnet.dev/zombieb/galvanic-corrosion>
Copyright (C) 2025 @zombieb (Discord / proxnet Gitea)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
import Logging from "@proxnet/undead-logging";
import { Config } from "../config.ts";
const log = new Logging("Steam");
const config = Config.getConfig();
interface AuthenticateUserTicketSuccess {
result: 'OK',
steamid: string,
ownersteamid: string,
vacbanned: boolean,
publisherbanned: boolean
}
interface AuthenticateUserTicketError {
errorcode: number,
errordesc: string
}
interface SteamRes {
response: {
error?: AuthenticateUserTicketError,
params?: AuthenticateUserTicketSuccess
}
}
export async function AuthenticateUserTicket(ticket: string, userid: string) {
if (!config.auth.steamkey) return true; // always authenticate if no steam API key was found
const params = new URLSearchParams();
params.append('key', config.auth.steamkey);
params.append('appid', "471710");
params.append('ticket', ticket);
const res = await fetch(`https://api.steampowered.com/ISteamUserAuth/AuthenticateUserTicket/v1?${params}`);
const resjson = (await res.json()) as SteamRes;
if (resjson.response.error) {
log.w(`Steam Authentication failed: (${resjson.response.error.errorcode}) ${resjson.response.error.errordesc}`);
return false;
}
log.d(JSON.stringify(resjson.response));
if (resjson.response.params) return resjson.response.params.steamid === userid && resjson.response.params.ownersteamid === userid;
else {
log.w("Steam Authentication failed: Steam response did not contain params or error! This should never be logged!");
return false;
}
}
export * as Steam from "./steam.ts";

View File

@@ -24,6 +24,7 @@ import { TokenBaseFormat } from "../apiutils.ts";
type UserInitOptions = {
client_id: string;
pubkey: string;
captcha?: string;
};
export enum AuthType {

View File

@@ -71,8 +71,7 @@ export const KeyGroups = {
},
Content: {
Root: "content",
Images: "images",
Rooms: "rooms",
Files: "file-meta",
},
Profile_Usernames: "profile-usernames",
PlatformAssociations: "platforms",

View File

@@ -29,6 +29,7 @@ import { SocketHandoff } from "./socket/handoff.ts";
import { SignalRSocketHandler } from "./socket/socket.ts";
import Rooms from "./data/content/rooms.ts";
import { GameConfigs } from "./data/config.ts";
import { RootPath } from "./data/content/baseimages.ts";
const instanceId = generateRandomString(64);
@@ -39,16 +40,16 @@ log.i(`Starting Galvanic Corrosion..`);
const config = Config.getConfig();
if (typeof config == "undefined") {
log.e("Cannot start: Configuration is undefined");
Deno.exit(1);
log.e("Cannot start: Configuration was not found.");
Deno.exit(5);
}
if (config.auth.secret == Config.defaultConfig.auth.secret) {
log.e(`Cannot start: Auth secret is default. Please change 'auth.secret' in 'config.json'`);
Deno.exit(1);
Deno.exit(5);
}
if (config.public.serverId == Config.defaultConfig.public.serverId) {
log.e(`Cannot start: Server ID is default. Please change 'public.serverId' in 'config.json'`);
Deno.exit(1);
Deno.exit(5);
}
Log.MessageTypeVisibility.Network = config.logging.network;
@@ -105,6 +106,8 @@ app.use(matchRouter.route.path, matchRouter.route.router);
app.use(notifyRouter.route.path, notifyRouter.route.router);
app.use(cdnRouter.route.path, cdnRouter.route.router);
// end content routes
app.use((rq: express.Request, rs: express.Response) => {
log.e(`${rs.locals.reqId} ${APIUtils.getSrcIpDefault(rq)} 404 ${rq.method} ${rq.url.toString()}`);
rs.statusCode = 404;
@@ -149,14 +152,6 @@ try {
const abort = new AbortController();
abort.signal.addEventListener('abort', () => {
log.n("Closing all sockets");
const sockets = UnifiedProfile.getAllSockets();
for (const socket of sockets.values()) {
socket.destroy(socket)();
} // I used the socket to destroy the socket
});
// Galvanic WebSocket
Deno.serve({port: config.web.socket.port, hostname: config.web.socket.host, signal: abort.signal, onListen: addr => {
log.n(`Socket listening on http://${addr.hostname}:${addr.port}`);

View File

@@ -103,12 +103,25 @@ route.router.get("/me",
);
route.router.post("/me/displayname",
interface DisplayNameUpdate {
displayName: string
}
const DisplayNameUpdateSchema = z.object({
displayName: z.string().max(24, "DisplayName too long!")
})
route.router.put("/me/displayname",
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
express.urlencoded({ extended: true }),
APIUtils.validateRequestBody(DisplayNameUpdateSchema),
(rq: express.Request<{}, {}, DisplayNameUpdate>, rs: express.Response, nxt: express.NextFunction) => {
rs.locals.profile.setDisplayName(rq.body.displayName);
nxt();
},
APIUtils.RecNetResponse(true, "DisplayName customization is not yet implemented."),
APIUtils.RecNetResponse(true, "Updated DisplayName.")
);

View File

@@ -34,7 +34,7 @@ route.router.get('/me/haspassword',
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Game),
(rq, rs) => {
(_rq, rs) => {
rs.json(true);
},

View File

@@ -25,6 +25,7 @@ import { z } from "zod";
import { AuthType } from "../../data/users.ts";
import { Redis } from "../../db.ts";
import { validVersions } from "../api/versioncheck.ts";
import { Steam } from "../../data/steam.ts";
const config = Config.getConfig();
@@ -53,9 +54,18 @@ interface RefreshRequest extends AuthBodyBase {
refresh_token: string,
grant_type: "refresh_token"
}
interface SteamPlatformParams {
Ticket: string,
AppId: string
}
type TokenRequestBody = TokenRequest | RefreshRequest;
const SteamPlatformParamsSchema = z.object({
Ticket: z.string(),
AppId: z.literal('471710')
});
const AuthBodyBaseSchema = z.object({
grant_type: z.string(),
client_id: z.string(),
@@ -98,6 +108,7 @@ route.router.post("/token",
APIUtils.Authentication,
APIUtils.AuthenticationType(AuthType.Web),
express.urlencoded({ extended: true }),
APIUtils.logBody,
APIUtils.validateRequestBody<AuthBodyBase>(TokenRequestBodySchema),
async (
@@ -125,6 +136,7 @@ route.router.post("/token",
!(rq.body.platform_id.length > 32),
!(rq.body.time.length > 32),
!(rq.body.asid.length > 32),
SteamPlatformParamsSchema.safeParse(JSON.parse(rq.body.platform_auth)).success
].includes(false);
if (!conditionsMet) {
@@ -153,6 +165,16 @@ route.router.post("/token",
targetAccount = parseInt(decodedToken.sub ? decodedToken.sub : "NaN");
}
const platformAuth = (JSON.parse(rq.body.platform_auth)) as SteamPlatformParams;
if (config.auth.steamkey) {
const steamAuthed = await Steam.AuthenticateUserTicket(platformAuth.Ticket, rq.body.platform_id);
if (!steamAuthed) {
requestFailed();
return;
}
}
if (isNaN(targetAccount)) {
requestFailed();

View File

@@ -18,7 +18,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { APIUtils, NoBody } from "../apiutils.ts";
import * as BaseImages from "../data/content/baseimages.ts";
import express from "express";
import * as Images from "./../data/content/images.ts";
import { Image } from "https://deno.land/x/imagescript@1.3.0/mod.ts";
import { Buffer } from "node:buffer";
@@ -30,8 +29,6 @@ function sanitizeString(input: string) {
return input.split("").filter((char) => chars.includes(char)).join("");
}
const baseImages = BaseImages.getAllBaseImages();
interface ImageQueryOptions {
cropSquare?: string;
width?: string;
@@ -50,10 +47,12 @@ route.router.get(
rq.path.substring(1, rq.path.length).replaceAll("%20", " "),
);
// deno-lint-ignore prefer-const
let image: Image;
const imageSource = baseImages.includes(filename)
/*const imageSource = baseImages.includes(filename)
? BaseImages.getBaseImage(filename)
: await Images.getImage(filename);
: await Images.getImage(filename);*/
const imageSource = BaseImages.getBaseImage(filename);
if (imageSource == null) {
nxt();
return;

View File

@@ -129,7 +129,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.`);
authFailed("Authentication request failed.");
return;
}

View File

@@ -34,6 +34,10 @@ import {
import { SocketTarget } from "./targets/targetbase.ts";
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
import Presence from "../data/live/presence.ts";
import Instances from "../data/live/instances.ts";
import Matchmaking from "../data/live/base.ts";
const logmessages = true;
export class SignalRSocketHandler {
@@ -55,7 +59,7 @@ export class SignalRSocketHandler {
player.setSocketHandler(this);
this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget());
this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget(this));
this.#PeriodicalId = setInterval(async () => {
if (this.#socket.readyState !== this.#socket.CLOSED) {
@@ -86,7 +90,7 @@ export class SignalRSocketHandler {
this.sendRaw({});
return;
} else {
//this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type]})\n ${JSON.stringify(message.data)}`);
if (logmessages) this.#log.d(`CLIENT MESSAGE\n Type: ${message.data.type} (${SignalMessageType[message.data.type]})\n ${JSON.stringify(message.data)}`);
if (message.data.type == SignalMessageType.Invocation && message.data.invocationId) { // don't send completion messages for nonblocking invocations
const res = await this.#dispatchTarget(message.data.target, message.data.arguments[0]); // rec room only uses the first index
if (res.type == TargetResultType.Success) {
@@ -153,14 +157,21 @@ export class SignalRSocketHandler {
if (!internal) sock.#socket.close();
sock.#log.i(`Closed socket`);
sock.#profile.clearSocketHandler();
for (const target of sock.#Targets.values()) target.destroy();
Instances.removePlayerFromCurrentInstance(this.#profile);
Matchmaking.deleteLoginLock(this.#profile);
}
}
sendRaw(data: object) {
this.#socket.send(`${JSON.stringify(data)}\u001e`);
// todo sometime: make the below less confusing
//const type = `Type: ${JSON.stringify(data) == '{}' ? 'Protocol Message' : `${(data as SignalRMessage).type} (${SignalMessageType[(data as SignalRMessage).type]})`}`;
//this.#log.d(`SERVER MESSAGE\n ${type}\n ${JSON.stringify(data as SignalRMessage)}`);
if (logmessages) {
const type = `Type: ${JSON.stringify(data) == '{}' ? 'Protocol Message' : `${(data as SignalRMessage).type} (${SignalMessageType[(data as SignalRMessage).type]})`}`;
this.#log.d(`SERVER MESSAGE\n ${type}\n ${JSON.stringify(data as SignalRMessage)}`);
}
}
sendNotification(id: PushNotificationId | string, args: object) {

View File

@@ -17,6 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { z } from "zod";
import { SocketTarget } from "./targetbase.ts";
import UnifiedProfile, { SelfAccountExport } from "../../data/profiles.ts";
import { ProfileEvents, ProfileUpdatedEvent } from "../../data/profileevents.ts";
import { PushNotificationId } from "../types.ts";
const ArgumentSchema = z.object({
PlayerIds: z.array(z.number())
@@ -24,10 +27,35 @@ const ArgumentSchema = z.object({
export class PlayerSocketSubscriptionTarget extends SocketTarget {
subscriptions: number[] = [];
updateSocket(profile: SelfAccountExport) {
this.socket.sendNotification(PushNotificationId.SubscriptionUpdateProfile, profile);
}
subscriptions: { id: number, callback: (ev: unknown) => void }[] = [];
setSubscriptions(subs: number[]) {
this.subscriptions = subs;
this.clearSubscriptions();
for (const id of subs)
this.subscriptions.push({ id: id, callback: UnifiedProfile.get(id)
.on<ProfileUpdatedEvent>(ProfileEvents.BaseUpdated, (async ev => {
const exported = await ev.profile.export();
if (exported) this.updateSocket(exported);
}
)) });
}
clearSubscriptions() {
for (const sub of this.subscriptions) {
const profile = UnifiedProfile.get(sub.id);
profile.off(ProfileEvents.BaseUpdated, sub.callback);
}
}
override destroy() {
this.clearSubscriptions();
}
// deno-lint-ignore require-await

View File

@@ -15,18 +15,18 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
import { SignalRSocketHandler } from "../socket.ts";
export class SocketTarget {
profileNotSetError = new Error("The profile on this target is not set.");
socket: SignalRSocketHandler;
profileId: number | null = null;
setProfile(id: number) {
this.profileId = id;
constructor(socket: SignalRSocketHandler) {
this.socket = socket;
}
profileIsSet() {
return this.profileId !== null;
destroy() {
return;
}
// deno-lint-ignore require-await