my power's back
All checks were successful
Galvanic Corrosion Cross-Compile / build (push) Successful in 1m57s
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:
27
CONFIG.md
27
CONFIG.md
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
[<-- Click to return to README.md](./README.md)
|
[<-- Click to return to README.md](./README.md)
|
||||||
|
|
||||||
|
We recommend that you store the configuration file `config.json` in a safe place where Galvanic Corrosion can access it (the current directory).<br>
|
||||||
|
No other user on your server system should be able to access the file.
|
||||||
|
|
||||||
## Redis
|
## Redis
|
||||||
Redis is database software and must be installed for Galvanic Corrosion.
|
Redis is database software and must be installed for Galvanic Corrosion.
|
||||||
|
|
||||||
@@ -41,7 +44,8 @@ This section contains basic information regarding your server.
|
|||||||
`serverName`: Somewhat invisible to players, but is an official label your server could appear as (to future server lists?)
|
`serverName`: Somewhat invisible to players, but is an official label your server could appear as (to future server lists?)
|
||||||
|
|
||||||
`serverId`: Used in the authentication process, uniquely identifies your server to clients. Players should never see this.<br>
|
`serverId`: Used in the authentication process, uniquely identifies your server to clients. Players should never see this.<br>
|
||||||
Ideally, this should be unique for every server, and can be chosen by the server administrator.
|
Ideally, this should be unique for every server, and can be chosen by the server administrator.<br>
|
||||||
|
Example: zombieb-cool-gc-server
|
||||||
|
|
||||||
`owner`: That's you! You can insert your handle for any social networking site or some form of identification here.
|
`owner`: That's you! You can insert your handle for any social networking site or some form of identification here.
|
||||||
|
|
||||||
@@ -57,20 +61,31 @@ Ideally, this should be unique for every server, and can be chosen by the server
|
|||||||
this can be anything *except* for "none" or 4, since there is only one server to connect to and the game uses offline mode when the region ID is set to none.
|
this can be anything *except* for "none" or 4, since there is only one server to connect to and the game uses offline mode when the region ID is set to none.
|
||||||
|
|
||||||
`initialRoom`: On game startup, redirects the player to this room name instead of their DormRoom. Set to null if a "natural" startup is preferred.<br>
|
`initialRoom`: On game startup, redirects the player to this room name instead of their DormRoom. Set to null if a "natural" startup is preferred.<br>
|
||||||
Ideally, this room should not be private and should be matchmakeable.
|
This room must not be private and must be matchmakeable.
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
These three values expose booleans you can change to enable/disable logging various messages sent by the server used for debugging or troubleshooting purposes.
|
These three values expose booleans you can change to enable/disable logging various messages used for debugging or troubleshooting purposes.
|
||||||
|
|
||||||
## Discord
|
## Discord
|
||||||
Currently unused.
|
Can be `null`. Currently unused.
|
||||||
|
|
||||||
A Discord Bot is planned for interacting with your server outside of the game.
|
A Discord Bot is planned for interacting with your server outside of the game.
|
||||||
|
|
||||||
## Auth
|
## Auth
|
||||||
Parameters used by the server's authentication mechanisms.
|
Parameters used by the server's authentication mechanisms.
|
||||||
|
|
||||||
`secret`: Used to generate tokens. should never be shared (by extension, the whole file) and can be a string of characters containing no words or patterns.
|
`secret`: Used to generate tokens. Should never be shared (the entire file) and can be a string of characters containing no words or patterns.
|
||||||
<br>Use secure cryptography APIs in programming languages to generate random strings.
|
<br>Use secure cryptography APIs in programming languages to generate random strings.
|
||||||
|
|
||||||
`timeout`: The maximum age for a token.
|
`timeout`: The maximum age for a token.
|
||||||
|
|
||||||
|
### Auth Verification
|
||||||
|
Can be `null`. Cloudflare Turnstile is used to verify users before they create their account.
|
||||||
|
|
||||||
|
`enabled`: `true` by default. This section may also be `null`.<br>
|
||||||
|
This enables the `GET /user/verify` endpoint that is presented to users wishing to create an account.<br>
|
||||||
|
`POST /user/verify` is used for Turnstile siteverify.
|
||||||
|
|
||||||
|
`sitekey`: Turnstile sitekey from Cloudflare dashboard
|
||||||
|
|
||||||
|
`secretkey`: Turnstile secretkey from Cloudflare dashboard
|
||||||
10
README.md
10
README.md
@@ -14,10 +14,8 @@ Galvanic Corrosion and its contributors are **not** associated in **any** form w
|
|||||||
* Exit Games Inc.
|
* Exit Games Inc.
|
||||||
* Against Gravity
|
* Against Gravity
|
||||||
* Photon Network, Photon Engine, or services associated
|
* Photon Network, Photon Engine, or services associated
|
||||||
* Services in use by or associated with the RecNet platform
|
|
||||||
* Any person(s) in contact with or employed for or with Rec Room Inc.
|
* Any person(s) in contact with or employed for or with Rec Room Inc.
|
||||||
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Read how to configure Galvanic Corrosion [here.](./CONFIG.md)
|
Read how to configure Galvanic Corrosion [here.](./CONFIG.md)
|
||||||
@@ -29,10 +27,4 @@ Read how to configure Galvanic Corrosion [here.](./CONFIG.md)
|
|||||||
- Configure project
|
- Configure project
|
||||||
- Clone this repo
|
- Clone this repo
|
||||||
- Install dependencies with `deno i`
|
- Install dependencies with `deno i`
|
||||||
- Compile server with `deno run cross-compile`
|
- Compile server with `deno run cross-compile`
|
||||||
|
|
||||||
## Client Patches
|
|
||||||
|
|
||||||
You can configure some client patches from the server. See the IL2CPP universal
|
|
||||||
patch for a list of patch IDs.
|
|
||||||
<br>Place desired patch ID strings into the config `public.patches`.
|
|
||||||
@@ -365,7 +365,7 @@ export function startTimer(_rq: express.Request, rs: express.Response, nxt: expr
|
|||||||
nxt();
|
nxt();
|
||||||
}
|
}
|
||||||
export function stopTimer(_rq: express.Request, rs: express.Response) {
|
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";
|
export * as APIUtils from "./apiutils.ts";
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ type AuthConfiguration = {
|
|||||||
* In Hours
|
* In Hours
|
||||||
*/
|
*/
|
||||||
timeout: number;
|
timeout: number;
|
||||||
|
steamkey: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GalvanicConfiguration = {
|
export type GalvanicConfiguration = {
|
||||||
@@ -124,6 +125,7 @@ export const defaultConfig: GalvanicConfiguration = {
|
|||||||
auth: {
|
auth: {
|
||||||
secret: "CHANGE-ME-PLEASE",
|
secret: "CHANGE-ME-PLEASE",
|
||||||
timeout: 3,
|
timeout: 3,
|
||||||
|
steamkey: null
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
65
src/data/baseevent.ts
Normal file
65
src/data/baseevent.ts
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ export function getConfig() {
|
|||||||
StartsInMinutes: 0,
|
StartsInMinutes: 0,
|
||||||
},
|
},
|
||||||
LevelProgressionMaps: generateLevelProgressionMap(),
|
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: {
|
AutoMicMutingConfig: {
|
||||||
MicSpamVolumeThreshold: 1.125,
|
MicSpamVolumeThreshold: 1.125,
|
||||||
MicVolumeSampleInterval: 0.25,
|
MicVolumeSampleInterval: 0.25,
|
||||||
|
|||||||
94
src/data/content/data.ts
Normal file
94
src/data/content/data.ts
Normal 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) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import UnifiedProfile, { Profile } from "../profiles.ts";
|
|||||||
import { RoomInstance, InstanceOptions } from "./types.ts";
|
import { RoomInstance, InstanceOptions } from "./types.ts";
|
||||||
import { Config } from "../../config.ts";
|
import { Config } from "../../config.ts";
|
||||||
import Presence from "./presence.ts";
|
import Presence from "./presence.ts";
|
||||||
|
import { RoomFactory } from "../content/rooms/RoomFactory.ts";
|
||||||
|
|
||||||
const log = new Logging("Instances");
|
const log = new Logging("Instances");
|
||||||
|
|
||||||
@@ -177,6 +178,9 @@ class InstancesBase {
|
|||||||
|
|
||||||
this.updateSingleInstanceIsFull(instance);
|
this.updateSingleInstanceIsFull(instance);
|
||||||
} else log.w(`Instance ${instance.roomInstanceId} is full. Cannot add player ${player.getId()}`);
|
} 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) {
|
playerIsInInstance(player: Profile, instance: RoomInstance) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { ProfileContentManager } from "./base/profilemanagerbase.ts";
|
|||||||
|
|
||||||
export class ProfileReputationManager extends ProfileContentManager {
|
export class ProfileReputationManager extends ProfileContentManager {
|
||||||
|
|
||||||
|
// deno-lint-ignore require-await
|
||||||
async getReputation() { // async temporary
|
async getReputation() { // async temporary
|
||||||
return {
|
return {
|
||||||
AccountId: this.profileId,
|
AccountId: this.profileId,
|
||||||
|
|||||||
@@ -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
|
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/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
import { Buffer } from "node:buffer";
|
import { Event } from "./baseevent.ts";
|
||||||
import { Redis } from "../../db.ts";
|
import { Profile } from "./profiles.ts";
|
||||||
|
|
||||||
export async function getImage(filename: string) {
|
export enum ProfileEvents {
|
||||||
const data = await Redis.Database.get(
|
BaseUpdated = "profile.updated"
|
||||||
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 interface ProfileUpdatedEvent extends Event {
|
||||||
|
profile: Profile
|
||||||
|
}
|
||||||
@@ -31,6 +31,8 @@ import { ProfileReputationManager } from "./profile/reputation.ts";
|
|||||||
import Logging from "@proxnet/undead-logging";
|
import Logging from "@proxnet/undead-logging";
|
||||||
import { ProfileRelationshipManager } from "./profile/relationships.ts";
|
import { ProfileRelationshipManager } from "./profile/relationships.ts";
|
||||||
import { ProfileAvatarManager } from "./profile/avatar.ts";
|
import { ProfileAvatarManager } from "./profile/avatar.ts";
|
||||||
|
import { EventManager } from "./baseevent.ts";
|
||||||
|
import { ProfileEvents, ProfileUpdatedEvent } from "./profileevents.ts";
|
||||||
|
|
||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|
||||||
@@ -53,7 +55,7 @@ interface AccountExport {
|
|||||||
username: string;
|
username: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
}
|
}
|
||||||
interface SelfAccountExport extends AccountExport {
|
export interface SelfAccountExport extends AccountExport {
|
||||||
email?: string,
|
email?: string,
|
||||||
phone?: string,
|
phone?: string,
|
||||||
juniorState?: number,
|
juniorState?: number,
|
||||||
@@ -67,7 +69,7 @@ export interface ProfileTokenFormat extends TokenBaseFormat {
|
|||||||
|
|
||||||
const reservedIds = [1, 2];
|
const reservedIds = [1, 2];
|
||||||
|
|
||||||
class Profile {
|
class Profile extends EventManager {
|
||||||
static async exists(id: number) {
|
static async exists(id: number) {
|
||||||
return (await Redis.Database.exists(
|
return (await Redis.Database.exists(
|
||||||
Redis.buildKey(
|
Redis.buildKey(
|
||||||
@@ -167,8 +169,7 @@ class Profile {
|
|||||||
isJunior: values[1] == null ? false : JSON.parse(values[1]),
|
isJunior: values[1] == null ? false : JSON.parse(values[1]),
|
||||||
platforms: 1,
|
platforms: 1,
|
||||||
username: values[2] == null ? "DATABASEERROR" : values[2],
|
username: values[2] == null ? "DATABASEERROR" : values[2],
|
||||||
displayName: values[3] == null ? (values[2] == null ? "DATABASEERROR" : values[2]) : values[3],
|
displayName: values[3] == null ? (values[2] == null ? "DATABASEERROR" : values[2]) : values[3]
|
||||||
email: "notanemail@notanemailsite.local" // we are confirmed
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -196,6 +197,8 @@ class Profile {
|
|||||||
Avatar: ProfileAvatarManager;
|
Avatar: ProfileAvatarManager;
|
||||||
|
|
||||||
constructor(id: number) {
|
constructor(id: number) {
|
||||||
|
super();
|
||||||
|
|
||||||
this.#id = id;
|
this.#id = id;
|
||||||
|
|
||||||
this.Settings = new ProfileSettingsManager(this.#id);
|
this.Settings = new ProfileSettingsManager(this.#id);
|
||||||
@@ -205,6 +208,14 @@ class Profile {
|
|||||||
this.Avatar = new ProfileAvatarManager(this.#id);
|
this.Avatar = new ProfileAvatarManager(this.#id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#emitProfileUpdated() {
|
||||||
|
const ev: ProfileUpdatedEvent = {
|
||||||
|
time: new Date(),
|
||||||
|
profile: this
|
||||||
|
}
|
||||||
|
this.emit(ProfileEvents.BaseUpdated, ev);
|
||||||
|
}
|
||||||
|
|
||||||
setInstance(instance: RoomInstance | null) {
|
setInstance(instance: RoomInstance | null) {
|
||||||
this.#instance = instance;
|
this.#instance = instance;
|
||||||
}
|
}
|
||||||
@@ -229,6 +240,21 @@ class Profile {
|
|||||||
|
|
||||||
async setBio(bio: string) {
|
async setBio(bio: string) {
|
||||||
await Redis.Database.set(Redis.buildKey(Redis.KeyGroups.Profiles.Root, this.#id.toString(), Redis.KeyGroups.Profiles.Bio), bio);
|
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() {
|
async export() {
|
||||||
|
|||||||
68
src/data/steam.ts
Normal file
68
src/data/steam.ts
Normal 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";
|
||||||
@@ -24,6 +24,7 @@ import { TokenBaseFormat } from "../apiutils.ts";
|
|||||||
type UserInitOptions = {
|
type UserInitOptions = {
|
||||||
client_id: string;
|
client_id: string;
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
|
captcha?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum AuthType {
|
export enum AuthType {
|
||||||
|
|||||||
@@ -71,8 +71,7 @@ export const KeyGroups = {
|
|||||||
},
|
},
|
||||||
Content: {
|
Content: {
|
||||||
Root: "content",
|
Root: "content",
|
||||||
Images: "images",
|
Files: "file-meta",
|
||||||
Rooms: "rooms",
|
|
||||||
},
|
},
|
||||||
Profile_Usernames: "profile-usernames",
|
Profile_Usernames: "profile-usernames",
|
||||||
PlatformAssociations: "platforms",
|
PlatformAssociations: "platforms",
|
||||||
|
|||||||
19
src/main.ts
19
src/main.ts
@@ -29,6 +29,7 @@ import { SocketHandoff } from "./socket/handoff.ts";
|
|||||||
import { SignalRSocketHandler } from "./socket/socket.ts";
|
import { SignalRSocketHandler } from "./socket/socket.ts";
|
||||||
import Rooms from "./data/content/rooms.ts";
|
import Rooms from "./data/content/rooms.ts";
|
||||||
import { GameConfigs } from "./data/config.ts";
|
import { GameConfigs } from "./data/config.ts";
|
||||||
|
import { RootPath } from "./data/content/baseimages.ts";
|
||||||
|
|
||||||
const instanceId = generateRandomString(64);
|
const instanceId = generateRandomString(64);
|
||||||
|
|
||||||
@@ -39,16 +40,16 @@ log.i(`Starting Galvanic Corrosion..`);
|
|||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|
||||||
if (typeof config == "undefined") {
|
if (typeof config == "undefined") {
|
||||||
log.e("Cannot start: Configuration is undefined");
|
log.e("Cannot start: Configuration was not found.");
|
||||||
Deno.exit(1);
|
Deno.exit(5);
|
||||||
}
|
}
|
||||||
if (config.auth.secret == Config.defaultConfig.auth.secret) {
|
if (config.auth.secret == Config.defaultConfig.auth.secret) {
|
||||||
log.e(`Cannot start: Auth secret is default. Please change 'auth.secret' in 'config.json'`);
|
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) {
|
if (config.public.serverId == Config.defaultConfig.public.serverId) {
|
||||||
log.e(`Cannot start: Server ID is default. Please change 'public.serverId' in 'config.json'`);
|
log.e(`Cannot start: Server ID is default. Please change 'public.serverId' in 'config.json'`);
|
||||||
Deno.exit(1);
|
Deno.exit(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.MessageTypeVisibility.Network = config.logging.network;
|
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(notifyRouter.route.path, notifyRouter.route.router);
|
||||||
app.use(cdnRouter.route.path, cdnRouter.route.router);
|
app.use(cdnRouter.route.path, cdnRouter.route.router);
|
||||||
|
|
||||||
|
// end content routes
|
||||||
|
|
||||||
app.use((rq: express.Request, rs: express.Response) => {
|
app.use((rq: express.Request, rs: express.Response) => {
|
||||||
log.e(`${rs.locals.reqId} ${APIUtils.getSrcIpDefault(rq)} 404 ${rq.method} ${rq.url.toString()}`);
|
log.e(`${rs.locals.reqId} ${APIUtils.getSrcIpDefault(rq)} 404 ${rq.method} ${rq.url.toString()}`);
|
||||||
rs.statusCode = 404;
|
rs.statusCode = 404;
|
||||||
@@ -149,14 +152,6 @@ try {
|
|||||||
|
|
||||||
const abort = new AbortController();
|
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
|
// Galvanic WebSocket
|
||||||
Deno.serve({port: config.web.socket.port, hostname: config.web.socket.host, signal: abort.signal, onListen: addr => {
|
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}`);
|
log.n(`Socket listening on http://${addr.hostname}:${addr.port}`);
|
||||||
|
|||||||
@@ -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.Authentication,
|
||||||
APIUtils.AuthenticationType(AuthType.Game),
|
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.")
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ route.router.get('/me/haspassword',
|
|||||||
APIUtils.Authentication,
|
APIUtils.Authentication,
|
||||||
APIUtils.AuthenticationType(AuthType.Game),
|
APIUtils.AuthenticationType(AuthType.Game),
|
||||||
|
|
||||||
(rq, rs) => {
|
(_rq, rs) => {
|
||||||
rs.json(true);
|
rs.json(true);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { z } from "zod";
|
|||||||
import { AuthType } from "../../data/users.ts";
|
import { AuthType } from "../../data/users.ts";
|
||||||
import { Redis } from "../../db.ts";
|
import { Redis } from "../../db.ts";
|
||||||
import { validVersions } from "../api/versioncheck.ts";
|
import { validVersions } from "../api/versioncheck.ts";
|
||||||
|
import { Steam } from "../../data/steam.ts";
|
||||||
|
|
||||||
const config = Config.getConfig();
|
const config = Config.getConfig();
|
||||||
|
|
||||||
@@ -53,9 +54,18 @@ interface RefreshRequest extends AuthBodyBase {
|
|||||||
refresh_token: string,
|
refresh_token: string,
|
||||||
grant_type: "refresh_token"
|
grant_type: "refresh_token"
|
||||||
}
|
}
|
||||||
|
interface SteamPlatformParams {
|
||||||
|
Ticket: string,
|
||||||
|
AppId: string
|
||||||
|
}
|
||||||
|
|
||||||
type TokenRequestBody = TokenRequest | RefreshRequest;
|
type TokenRequestBody = TokenRequest | RefreshRequest;
|
||||||
|
|
||||||
|
const SteamPlatformParamsSchema = z.object({
|
||||||
|
Ticket: z.string(),
|
||||||
|
AppId: z.literal('471710')
|
||||||
|
});
|
||||||
|
|
||||||
const AuthBodyBaseSchema = z.object({
|
const AuthBodyBaseSchema = z.object({
|
||||||
grant_type: z.string(),
|
grant_type: z.string(),
|
||||||
client_id: z.string(),
|
client_id: z.string(),
|
||||||
@@ -98,6 +108,7 @@ route.router.post("/token",
|
|||||||
APIUtils.Authentication,
|
APIUtils.Authentication,
|
||||||
APIUtils.AuthenticationType(AuthType.Web),
|
APIUtils.AuthenticationType(AuthType.Web),
|
||||||
express.urlencoded({ extended: true }),
|
express.urlencoded({ extended: true }),
|
||||||
|
APIUtils.logBody,
|
||||||
APIUtils.validateRequestBody<AuthBodyBase>(TokenRequestBodySchema),
|
APIUtils.validateRequestBody<AuthBodyBase>(TokenRequestBodySchema),
|
||||||
|
|
||||||
async (
|
async (
|
||||||
@@ -125,6 +136,7 @@ route.router.post("/token",
|
|||||||
!(rq.body.platform_id.length > 32),
|
!(rq.body.platform_id.length > 32),
|
||||||
!(rq.body.time.length > 32),
|
!(rq.body.time.length > 32),
|
||||||
!(rq.body.asid.length > 32),
|
!(rq.body.asid.length > 32),
|
||||||
|
SteamPlatformParamsSchema.safeParse(JSON.parse(rq.body.platform_auth)).success
|
||||||
].includes(false);
|
].includes(false);
|
||||||
|
|
||||||
if (!conditionsMet) {
|
if (!conditionsMet) {
|
||||||
@@ -153,6 +165,16 @@ route.router.post("/token",
|
|||||||
targetAccount = parseInt(decodedToken.sub ? decodedToken.sub : "NaN");
|
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)) {
|
if (isNaN(targetAccount)) {
|
||||||
requestFailed();
|
requestFailed();
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
import { APIUtils, NoBody } from "../apiutils.ts";
|
import { APIUtils, NoBody } from "../apiutils.ts";
|
||||||
import * as BaseImages from "../data/content/baseimages.ts";
|
import * as BaseImages from "../data/content/baseimages.ts";
|
||||||
import express from "express";
|
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 { Image } from "https://deno.land/x/imagescript@1.3.0/mod.ts";
|
||||||
import { Buffer } from "node:buffer";
|
import { Buffer } from "node:buffer";
|
||||||
|
|
||||||
@@ -30,8 +29,6 @@ function sanitizeString(input: string) {
|
|||||||
return input.split("").filter((char) => chars.includes(char)).join("");
|
return input.split("").filter((char) => chars.includes(char)).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseImages = BaseImages.getAllBaseImages();
|
|
||||||
|
|
||||||
interface ImageQueryOptions {
|
interface ImageQueryOptions {
|
||||||
cropSquare?: string;
|
cropSquare?: string;
|
||||||
width?: string;
|
width?: string;
|
||||||
@@ -50,10 +47,12 @@ route.router.get(
|
|||||||
rq.path.substring(1, rq.path.length).replaceAll("%20", " "),
|
rq.path.substring(1, rq.path.length).replaceAll("%20", " "),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// deno-lint-ignore prefer-const
|
||||||
let image: Image;
|
let image: Image;
|
||||||
const imageSource = baseImages.includes(filename)
|
/*const imageSource = baseImages.includes(filename)
|
||||||
? BaseImages.getBaseImage(filename)
|
? BaseImages.getBaseImage(filename)
|
||||||
: await Images.getImage(filename);
|
: await Images.getImage(filename);*/
|
||||||
|
const imageSource = BaseImages.getBaseImage(filename);
|
||||||
if (imageSource == null) {
|
if (imageSource == null) {
|
||||||
nxt();
|
nxt();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ route.router.post("/auth",
|
|||||||
} else user = obj;
|
} else user = obj;
|
||||||
}
|
}
|
||||||
if (!(await user.addNonce(rq.body.message.nonce))) {
|
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.");
|
authFailed("Authentication request failed.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ import {
|
|||||||
import { SocketTarget } from "./targets/targetbase.ts";
|
import { SocketTarget } from "./targets/targetbase.ts";
|
||||||
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
|
import { PlayerSocketSubscriptionTarget } from "./targets/SubscribeToPlayers.ts";
|
||||||
import Presence from "../data/live/presence.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 {
|
export class SignalRSocketHandler {
|
||||||
|
|
||||||
@@ -55,7 +59,7 @@ export class SignalRSocketHandler {
|
|||||||
|
|
||||||
player.setSocketHandler(this);
|
player.setSocketHandler(this);
|
||||||
|
|
||||||
this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget());
|
this.#Targets.set('SubscribeToPlayers', new PlayerSocketSubscriptionTarget(this));
|
||||||
|
|
||||||
this.#PeriodicalId = setInterval(async () => {
|
this.#PeriodicalId = setInterval(async () => {
|
||||||
if (this.#socket.readyState !== this.#socket.CLOSED) {
|
if (this.#socket.readyState !== this.#socket.CLOSED) {
|
||||||
@@ -86,7 +90,7 @@ export class SignalRSocketHandler {
|
|||||||
this.sendRaw({});
|
this.sendRaw({});
|
||||||
return;
|
return;
|
||||||
} else {
|
} 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
|
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
|
const res = await this.#dispatchTarget(message.data.target, message.data.arguments[0]); // rec room only uses the first index
|
||||||
if (res.type == TargetResultType.Success) {
|
if (res.type == TargetResultType.Success) {
|
||||||
@@ -153,14 +157,21 @@ export class SignalRSocketHandler {
|
|||||||
if (!internal) sock.#socket.close();
|
if (!internal) sock.#socket.close();
|
||||||
sock.#log.i(`Closed socket`);
|
sock.#log.i(`Closed socket`);
|
||||||
sock.#profile.clearSocketHandler();
|
sock.#profile.clearSocketHandler();
|
||||||
|
|
||||||
|
for (const target of sock.#Targets.values()) target.destroy();
|
||||||
|
|
||||||
|
Instances.removePlayerFromCurrentInstance(this.#profile);
|
||||||
|
Matchmaking.deleteLoginLock(this.#profile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sendRaw(data: object) {
|
sendRaw(data: object) {
|
||||||
this.#socket.send(`${JSON.stringify(data)}\u001e`);
|
this.#socket.send(`${JSON.stringify(data)}\u001e`);
|
||||||
// todo sometime: make the below less confusing
|
// todo sometime: make the below less confusing
|
||||||
//const type = `Type: ${JSON.stringify(data) == '{}' ? 'Protocol Message' : `${(data as SignalRMessage).type} (${SignalMessageType[(data as SignalRMessage).type]})`}`;
|
if (logmessages) {
|
||||||
//this.#log.d(`SERVER MESSAGE\n ${type}\n ${JSON.stringify(data as SignalRMessage)}`);
|
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) {
|
sendNotification(id: PushNotificationId | string, args: object) {
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { SocketTarget } from "./targetbase.ts";
|
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({
|
const ArgumentSchema = z.object({
|
||||||
PlayerIds: z.array(z.number())
|
PlayerIds: z.array(z.number())
|
||||||
@@ -24,10 +27,35 @@ const ArgumentSchema = z.object({
|
|||||||
|
|
||||||
export class PlayerSocketSubscriptionTarget extends SocketTarget {
|
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[]) {
|
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
|
// deno-lint-ignore require-await
|
||||||
|
|||||||
@@ -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
|
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/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
|
|
||||||
|
import { SignalRSocketHandler } from "../socket.ts";
|
||||||
|
|
||||||
export class SocketTarget {
|
export class SocketTarget {
|
||||||
|
|
||||||
profileNotSetError = new Error("The profile on this target is not set.");
|
socket: SignalRSocketHandler;
|
||||||
|
|
||||||
profileId: number | null = null;
|
constructor(socket: SignalRSocketHandler) {
|
||||||
|
this.socket = socket;
|
||||||
setProfile(id: number) {
|
|
||||||
this.profileId = id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
profileIsSet() {
|
destroy() {
|
||||||
return this.profileId !== null;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// deno-lint-ignore require-await
|
// deno-lint-ignore require-await
|
||||||
|
|||||||
Reference in New Issue
Block a user