diff --git a/.github/workflows/continuous_integration.yml b/.github/workflows/continuous_integration.yml index 2036e4e6..faf50c7a 100644 --- a/.github/workflows/continuous_integration.yml +++ b/.github/workflows/continuous_integration.yml @@ -64,6 +64,11 @@ jobs: run: yarn test working-directory: "front" + # We will enable prettier checks on front in a few month, when most PRs without prettier have been merged + # - name: "Prettier" + # run: yarn run pretty-check + # working-directory: "front" + continuous-integration-pusher: name: "Continuous Integration Pusher" @@ -107,6 +112,10 @@ jobs: run: yarn test working-directory: "pusher" + - name: "Prettier" + run: yarn run pretty-check + working-directory: "pusher" + continuous-integration-back: name: "Continuous Integration Back" @@ -150,3 +159,7 @@ jobs: run: yarn test working-directory: "back" + - name: "Prettier" + run: yarn run pretty-check + working-directory: "back" + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5cd50dc..b85d0a98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,15 @@ $ yarn run install $ yarn run prepare ``` +If you don't have the precommit hook installed (or if you committed code before installing the precommit hook), you will need +to run code linting manually: + +```console +$ docker-compose exec front yarn run pretty +$ docker-compose exec pusher yarn run pretty +$ docker-compose exec back yarn run pretty +``` + ### Providing tests WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test. diff --git a/back/package.json b/back/package.json index 5bf5d031..7015b9b8 100644 --- a/back/package.json +++ b/back/package.json @@ -10,8 +10,8 @@ "runprod": "node --max-old-space-size=4096 ./dist/server.js", "profile": "tsc && node --prof ./dist/server.js", "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", - "lint": "node_modules/.bin/eslint src/ . --ext .ts", - "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", + "lint": "DEBUG= node_modules/.bin/eslint src/ . --ext .ts", + "fix": "DEBUG= node_modules/.bin/eslint --fix src/ . --ext .ts", "precommit": "lint-staged", "pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'", "pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'" diff --git a/back/src/App.ts b/back/src/App.ts index 4bcc56ba..a6c42abb 100644 --- a/back/src/App.ts +++ b/back/src/App.ts @@ -1,7 +1,7 @@ // lib/app.ts -import {PrometheusController} from "./Controller/PrometheusController"; -import {DebugController} from "./Controller/DebugController"; -import {App as uwsApp} from "./Server/sifrr.server"; +import { PrometheusController } from "./Controller/PrometheusController"; +import { DebugController } from "./Controller/DebugController"; +import { App as uwsApp } from "./Server/sifrr.server"; class App { public app: uwsApp; diff --git a/back/src/Controller/BaseController.ts b/back/src/Controller/BaseController.ts index 93c17ab4..dc510d6c 100644 --- a/back/src/Controller/BaseController.ts +++ b/back/src/Controller/BaseController.ts @@ -1,10 +1,9 @@ -import {HttpResponse} from "uWebSockets.js"; - +import { HttpResponse } from "uWebSockets.js"; export class BaseController { protected addCorsHeaders(res: HttpResponse): void { - res.writeHeader('access-control-allow-headers', 'Origin, X-Requested-With, Content-Type, Accept'); - res.writeHeader('access-control-allow-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); - res.writeHeader('access-control-allow-origin', '*'); + res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept"); + res.writeHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE"); + res.writeHeader("access-control-allow-origin", "*"); } } diff --git a/back/src/Controller/DebugController.ts b/back/src/Controller/DebugController.ts index 509d8b2f..b7f037fd 100644 --- a/back/src/Controller/DebugController.ts +++ b/back/src/Controller/DebugController.ts @@ -1,53 +1,54 @@ -import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; -import {stringify} from "circular-json"; -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -import { parse } from 'query-string'; -import {App} from "../Server/sifrr.server"; -import {socketManager} from "../Services/SocketManager"; +import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable"; +import { stringify } from "circular-json"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; +import { parse } from "query-string"; +import { App } from "../Server/sifrr.server"; +import { socketManager } from "../Services/SocketManager"; export class DebugController { - constructor(private App : App) { + constructor(private App: App) { this.getDump(); } - - getDump(){ + getDump() { this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => { const query = parse(req.getQuery()); if (query.token !== ADMIN_API_TOKEN) { - return res.status(401).send('Invalid token sent!'); + return res.status(401).send("Invalid token sent!"); } - return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify( - socketManager.getWorlds(), - (key: unknown, value: unknown) => { - if (key === 'listeners') { - return 'Listeners'; - } - if (key === 'socket') { - return 'Socket'; - } - if (key === 'batchedMessages') { - return 'BatchedMessages'; - } - if(value instanceof Map) { - const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any - for (const [mapKey, mapValue] of value.entries()) { - obj[mapKey] = mapValue; + return res + .writeStatus("200 OK") + .writeHeader("Content-Type", "application/json") + .end( + stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => { + if (key === "listeners") { + return "Listeners"; } - return obj; - } else if(value instanceof Set) { + if (key === "socket") { + return "Socket"; + } + if (key === "batchedMessages") { + return "BatchedMessages"; + } + if (value instanceof Map) { + const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any + for (const [mapKey, mapValue] of value.entries()) { + obj[mapKey] = mapValue; + } + return obj; + } else if (value instanceof Set) { const obj: Array = []; for (const [setKey, setValue] of value.entries()) { obj.push(setValue); } return obj; - } else { - return value; - } - } - )); + } else { + return value; + } + }) + ); }); } } diff --git a/back/src/Controller/PrometheusController.ts b/back/src/Controller/PrometheusController.ts index e854cf43..3ab3d33f 100644 --- a/back/src/Controller/PrometheusController.ts +++ b/back/src/Controller/PrometheusController.ts @@ -1,7 +1,7 @@ -import {App} from "../Server/sifrr.server"; -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -const register = require('prom-client').register; -const collectDefaultMetrics = require('prom-client').collectDefaultMetrics; +import { App } from "../Server/sifrr.server"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; +const register = require("prom-client").register; +const collectDefaultMetrics = require("prom-client").collectDefaultMetrics; export class PrometheusController { constructor(private App: App) { @@ -14,7 +14,7 @@ export class PrometheusController { } private metrics(res: HttpResponse, req: HttpRequest): void { - res.writeHeader('Content-Type', register.contentType); + res.writeHeader("Content-Type", register.contentType); res.end(register.metrics()); } } diff --git a/back/src/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts index 81693a98..19eddd3e 100644 --- a/back/src/Enum/EnvironmentVariable.ts +++ b/back/src/Enum/EnvironmentVariable.ts @@ -1,17 +1,17 @@ const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64; const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; -const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false; -const ADMIN_API_URL = process.env.ADMIN_API_URL || ''; -const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken'; +const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false; +const ADMIN_API_URL = process.env.ADMIN_API_URL || ""; +const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken"; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; -const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; -const JITSI_ISS = process.env.JITSI_ISS || ''; -const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ''; -const HTTP_PORT = parseInt(process.env.HTTP_PORT || '8080') || 8080; -const GRPC_PORT = parseInt(process.env.GRPC_PORT || '50051') || 50051; +const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; +const JITSI_ISS = process.env.JITSI_ISS || ""; +const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ""; +const HTTP_PORT = parseInt(process.env.HTTP_PORT || "8080") || 8080; +const GRPC_PORT = parseInt(process.env.GRPC_PORT || "50051") || 50051; export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed -export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || ''; -export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4'); +export const TURN_STATIC_AUTH_SECRET = process.env.TURN_STATIC_AUTH_SECRET || ""; +export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4"); export { MINIMUM_DISTANCE, @@ -24,5 +24,5 @@ export { CPU_OVERHEAT_THRESHOLD, JITSI_URL, JITSI_ISS, - SECRET_JITSI_KEY -} + SECRET_JITSI_KEY, +}; diff --git a/back/src/Model/Admin.ts b/back/src/Model/Admin.ts index 29b53385..93396fa8 100644 --- a/back/src/Model/Admin.ts +++ b/back/src/Model/Admin.ts @@ -1,15 +1,12 @@ import { ServerToAdminClientMessage, - UserJoinedRoomMessage, UserLeftRoomMessage + UserJoinedRoomMessage, + UserLeftRoomMessage, } from "../Messages/generated/messages_pb"; -import {AdminSocket} from "../RoomManager"; - +import { AdminSocket } from "../RoomManager"; export class Admin { - public constructor( - private readonly socket: AdminSocket - ) { - } + public constructor(private readonly socket: AdminSocket) {} public sendUserJoin(uuid: string, name: string, ip: string): void { const serverToAdminClientMessage = new ServerToAdminClientMessage(); @@ -24,7 +21,7 @@ export class Admin { this.socket.write(serverToAdminClientMessage); } - public sendUserLeft(uuid: string/*, name: string, ip: string*/): void { + public sendUserLeft(uuid: string /*, name: string, ip: string*/): void { const serverToAdminClientMessage = new ServerToAdminClientMessage(); const userLeftRoomMessage = new UserLeftRoomMessage(); diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index 53d0a855..020f4c29 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -1,16 +1,16 @@ -import {PointInterface} from "./Websocket/PointInterface"; -import {Group} from "./Group"; -import {User, UserSocket} from "./User"; -import {PositionInterface} from "_Model/PositionInterface"; -import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone"; -import {PositionNotifier} from "./PositionNotifier"; -import {Movable} from "_Model/Movable"; -import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier"; -import {arrayIntersect} from "../Services/ArrayHelper"; -import {EmoteEventMessage, JoinRoomMessage} from "../Messages/generated/messages_pb"; -import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {ZoneSocket} from "src/RoomManager"; -import {Admin} from "../Model/Admin"; +import { PointInterface } from "./Websocket/PointInterface"; +import { Group } from "./Group"; +import { User, UserSocket } from "./User"; +import { PositionInterface } from "_Model/PositionInterface"; +import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback } from "_Model/Zone"; +import { PositionNotifier } from "./PositionNotifier"; +import { Movable } from "_Model/Movable"; +import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier"; +import { arrayIntersect } from "../Services/ArrayHelper"; +import { EmoteEventMessage, JoinRoomMessage } from "../Messages/generated/messages_pb"; +import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; +import { ZoneSocket } from "src/RoomManager"; +import { Admin } from "../Model/Admin"; export type ConnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void; @@ -39,33 +39,33 @@ export class GameRoom { private readonly positionNotifier: PositionNotifier; public readonly roomId: string; public readonly roomSlug: string; - public readonly worldSlug: string = ''; - public readonly organizationSlug: string = ''; - private versionNumber:number = 1; + public readonly worldSlug: string = ""; + public readonly organizationSlug: string = ""; + private versionNumber: number = 1; private nextUserId: number = 1; - constructor(roomId: string, - connectCallback: ConnectCallback, - disconnectCallback: DisconnectCallback, - minDistance: number, - groupRadius: number, - onEnters: EntersCallback, - onMoves: MovesCallback, - onLeaves: LeavesCallback, - onEmote: EmoteCallback, + constructor( + roomId: string, + connectCallback: ConnectCallback, + disconnectCallback: DisconnectCallback, + minDistance: number, + groupRadius: number, + onEnters: EntersCallback, + onMoves: MovesCallback, + onLeaves: LeavesCallback, + onEmote: EmoteCallback ) { this.roomId = roomId; if (isRoomAnonymous(roomId)) { this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); } else { - const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); + const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId); this.roomSlug = roomSlug; this.organizationSlug = organizationSlug; this.worldSlug = worldSlug; } - this.users = new Map(); this.usersByUuid = new Map(); this.admins = new Set(); @@ -86,21 +86,22 @@ export class GameRoom { return this.users; } - public getUserByUuid(uuid: string): User|undefined { + public getUserByUuid(uuid: string): User | undefined { return this.usersByUuid.get(uuid); } - public getUserById(id: number): User|undefined { + public getUserById(id: number): User | undefined { return this.users.get(id); } - - public join(socket : UserSocket, joinRoomMessage: JoinRoomMessage): User { + + public join(socket: UserSocket, joinRoomMessage: JoinRoomMessage): User { const positionMessage = joinRoomMessage.getPositionmessage(); if (positionMessage === undefined) { - throw new Error('Missing position message'); + throw new Error("Missing position message"); } const position = ProtobufUtils.toPointInterface(positionMessage); - const user = new User(this.nextUserId, + const user = new User( + this.nextUserId, joinRoomMessage.getUseruuid(), joinRoomMessage.getIpaddress(), position, @@ -126,12 +127,12 @@ export class GameRoom { return user; } - public leave(user : User){ + public leave(user: User) { const userObj = this.users.get(user.id); if (userObj === undefined) { - console.warn('User ', user.id, 'does not belong to this game room! It should!'); + console.warn("User ", user.id, "does not belong to this game room! It should!"); } - if (userObj !== undefined && typeof userObj.group !== 'undefined') { + if (userObj !== undefined && typeof userObj.group !== "undefined") { this.leaveGroup(userObj); } this.users.delete(user.id); @@ -143,7 +144,7 @@ export class GameRoom { // Notify admins for (const admin of this.admins) { - admin.sendUserLeft(user.uuid/*, user.name, user.IPAddress*/); + admin.sendUserLeft(user.uuid /*, user.name, user.IPAddress*/); } } @@ -151,7 +152,7 @@ export class GameRoom { return this.users.size === 0 && this.admins.size === 0; } - public updatePosition(user : User, userPosition: PointInterface): void { + public updatePosition(user: User, userPosition: PointInterface): void { user.setPosition(userPosition); this.updateUserGroup(user); @@ -173,22 +174,24 @@ export class GameRoom { return; } - const closestItem: User|Group|null = this.searchClosestAvailableUserOrGroup(user); + const closestItem: User | Group | null = this.searchClosestAvailableUserOrGroup(user); if (closestItem !== null) { if (closestItem instanceof Group) { // Let's join the group! closestItem.join(user); } else { - const closestUser : User = closestItem; - const group: Group = new Group(this.roomId,[ - user, - closestUser - ], this.connectCallback, this.disconnectCallback, this.positionNotifier); + const closestUser: User = closestItem; + const group: Group = new Group( + this.roomId, + [user, closestUser], + this.connectCallback, + this.disconnectCallback, + this.positionNotifier + ); this.groups.add(group); } } - } else { // If the user is part of a group: // should he leave the group? @@ -229,7 +232,9 @@ export class GameRoom { this.positionNotifier.leave(group); group.destroy(); if (!this.groups.has(group)) { - throw new Error("Could not find group "+group.getId()+" referenced by user "+user.id+" in World."); + throw new Error( + "Could not find group " + group.getId() + " referenced by user " + user.id + " in World." + ); } this.groups.delete(group); //todo: is the group garbage collected? @@ -247,16 +252,15 @@ export class GameRoom { * OR * - close enough to a group (distance <= groupRadius) */ - private searchClosestAvailableUserOrGroup(user: User): User|Group|null - { + private searchClosestAvailableUserOrGroup(user: User): User | Group | null { let minimumDistanceFound: number = Math.max(this.minDistance, this.groupRadius); let matchingItem: User | Group | null = null; this.users.forEach((currentUser, userId) => { // Let's only check users that are not part of a group - if (typeof currentUser.group !== 'undefined') { + if (typeof currentUser.group !== "undefined") { return; } - if(currentUser === user) { + if (currentUser === user) { return; } if (currentUser.silent) { @@ -265,7 +269,7 @@ export class GameRoom { const distance = GameRoom.computeDistance(user, currentUser); // compute distance between peers. - if(distance <= minimumDistanceFound && distance <= this.minDistance) { + if (distance <= minimumDistanceFound && distance <= this.minDistance) { minimumDistanceFound = distance; matchingItem = currentUser; } @@ -276,7 +280,7 @@ export class GameRoom { return; } const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), group.getPosition()); - if(distance <= minimumDistanceFound && distance <= this.groupRadius) { + if (distance <= minimumDistanceFound && distance <= this.groupRadius) { minimumDistanceFound = distance; matchingItem = group; } @@ -285,15 +289,15 @@ export class GameRoom { return matchingItem; } - public static computeDistance(user1: User, user2: User): number - { + public static computeDistance(user1: User, user2: User): number { const user1Position = user1.getPosition(); const user2Position = user2.getPosition(); - return Math.sqrt(Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2)); + return Math.sqrt( + Math.pow(user2Position.x - user1Position.x, 2) + Math.pow(user2Position.y - user1Position.y, 2) + ); } - public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number - { + public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number { return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2)); } @@ -325,9 +329,9 @@ export class GameRoom { public adminLeave(admin: Admin): void { this.admins.delete(admin); } - + public incrementVersion(): number { - this.versionNumber++ + this.versionNumber++; return this.versionNumber; } diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index ffe7a78a..5a0f3be6 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -1,13 +1,12 @@ import { ConnectCallback, DisconnectCallback } from "./GameRoom"; import { User } from "./User"; -import {PositionInterface} from "_Model/PositionInterface"; -import {Movable} from "_Model/Movable"; -import {PositionNotifier} from "_Model/PositionNotifier"; -import {gaugeManager} from "../Services/GaugeManager"; -import {MAX_PER_GROUP} from "../Enum/EnvironmentVariable"; +import { PositionInterface } from "_Model/PositionInterface"; +import { Movable } from "_Model/Movable"; +import { PositionNotifier } from "_Model/PositionNotifier"; +import { gaugeManager } from "../Services/GaugeManager"; +import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable"; export class Group implements Movable { - private static nextId: number = 1; private id: number; @@ -18,8 +17,13 @@ export class Group implements Movable { private wasDestroyed: boolean = false; private roomId: string; - - constructor(roomId: string, users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) { + constructor( + roomId: string, + users: User[], + private connectCallback: ConnectCallback, + private disconnectCallback: DisconnectCallback, + private positionNotifier: PositionNotifier + ) { this.roomId = roomId; this.users = new Set(); this.id = Group.nextId; @@ -43,7 +47,7 @@ export class Group implements Movable { return Array.from(this.users.values()); } - getId() : number { + getId(): number { return this.id; } @@ -53,7 +57,7 @@ export class Group implements Movable { getPosition(): PositionInterface { return { x: this.x, - y: this.y + y: this.y, }; } @@ -83,7 +87,7 @@ export class Group implements Movable { if (oldX === undefined) { this.positionNotifier.enter(this); } else { - this.positionNotifier.updatePosition(this, {x, y}, {x: oldX, y: oldY}); + this.positionNotifier.updatePosition(this, { x, y }, { x: oldX, y: oldY }); } } @@ -95,19 +99,17 @@ export class Group implements Movable { return this.users.size <= 1; } - join(user: User): void - { + join(user: User): void { // Broadcast on the right event this.connectCallback(user, this); this.users.add(user); user.group = this; } - leave(user: User): void - { + leave(user: User): void { const success = this.users.delete(user); if (success === false) { - throw new Error("Could not find user "+user.id+" in the group "+this.id); + throw new Error("Could not find user " + user.id + " in the group " + this.id); } user.group = undefined; @@ -123,8 +125,7 @@ export class Group implements Movable { * Let's kick everybody out. * Usually used when there is only one user left. */ - destroy(): void - { + destroy(): void { if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId); for (const user of this.users) { this.leave(user); @@ -132,7 +133,7 @@ export class Group implements Movable { this.wasDestroyed = true; } - get getSize(){ + get getSize() { return this.users.size; } } diff --git a/back/src/Model/Movable.ts b/back/src/Model/Movable.ts index 173db0ae..ca586b7c 100644 --- a/back/src/Model/Movable.ts +++ b/back/src/Model/Movable.ts @@ -1,8 +1,8 @@ -import {PositionInterface} from "_Model/PositionInterface"; +import { PositionInterface } from "_Model/PositionInterface"; /** * A physical object that can be placed into a Zone */ export interface Movable { - getPosition(): PositionInterface + getPosition(): PositionInterface; } diff --git a/back/src/Model/PositionInterface.ts b/back/src/Model/PositionInterface.ts index d3b0dd47..65636759 100644 --- a/back/src/Model/PositionInterface.ts +++ b/back/src/Model/PositionInterface.ts @@ -1,4 +1,4 @@ export interface PositionInterface { - x: number, - y: number + x: number; + y: number; } diff --git a/back/src/Model/PositionNotifier.ts b/back/src/Model/PositionNotifier.ts index 275bf9d0..c34c1ef1 100644 --- a/back/src/Model/PositionNotifier.ts +++ b/back/src/Model/PositionNotifier.ts @@ -8,12 +8,12 @@ * The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted * number of players around the current player. */ -import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone"; -import {Movable} from "_Model/Movable"; -import {PositionInterface} from "_Model/PositionInterface"; -import {ZoneSocket} from "../RoomManager"; -import {User} from "_Model/User"; -import {EmoteEventMessage} from "../Messages/generated/messages_pb"; +import { EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone } from "./Zone"; +import { Movable } from "_Model/Movable"; +import { PositionInterface } from "_Model/PositionInterface"; +import { ZoneSocket } from "../RoomManager"; +import { User } from "_Model/User"; +import { EmoteEventMessage } from "../Messages/generated/messages_pb"; interface ZoneDescriptor { i: number; @@ -21,19 +21,24 @@ interface ZoneDescriptor { } export class PositionNotifier { - // TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) private zones: Zone[][] = []; - constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: EntersCallback, private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback, private onEmote: EmoteCallback) { - } + constructor( + private zoneWidth: number, + private zoneHeight: number, + private onUserEnters: EntersCallback, + private onUserMoves: MovesCallback, + private onUserLeaves: LeavesCallback, + private onEmote: EmoteCallback + ) {} private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor { return { i: Math.floor(x / this.zoneWidth), j: Math.floor(y / this.zoneHeight), - } + }; } public enter(thing: Movable): void { @@ -100,6 +105,5 @@ export class PositionNotifier { const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y); const zone = this.getZone(zoneDesc.i, zoneDesc.j); zone.emitEmoteEvent(emoteEventMessage); - } } diff --git a/back/src/Model/RoomIdentifier.ts b/back/src/Model/RoomIdentifier.ts index 3ac62bca..d1de8800 100644 --- a/back/src/Model/RoomIdentifier.ts +++ b/back/src/Model/RoomIdentifier.ts @@ -1,30 +1,30 @@ //helper functions to parse room IDs export const isRoomAnonymous = (roomID: string): boolean => { - if (roomID.startsWith('_/')) { + if (roomID.startsWith("_/")) { return true; - } else if(roomID.startsWith('@/')) { + } else if (roomID.startsWith("@/")) { return false; } else { - throw new Error('Incorrect room ID: '+roomID); + throw new Error("Incorrect room ID: " + roomID); } -} +}; export const extractRoomSlugPublicRoomId = (roomId: string): string => { - const idParts = roomId.split('/'); - if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId); - return idParts.slice(2).join('/'); -} + const idParts = roomId.split("/"); + if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId); + return idParts.slice(2).join("/"); +}; export interface extractDataFromPrivateRoomIdResponse { organizationSlug: string; worldSlug: string; roomSlug: string; } export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => { - const idParts = roomId.split('/'); - if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId); + const idParts = roomId.split("/"); + if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId); const organizationSlug = idParts[1]; const worldSlug = idParts[2]; const roomSlug = idParts[3]; - return {organizationSlug, worldSlug, roomSlug} -} \ No newline at end of file + return { organizationSlug, worldSlug, roomSlug }; +}; diff --git a/back/src/Model/User.ts b/back/src/Model/User.ts index 4a3e75ec..186fb32a 100644 --- a/back/src/Model/User.ts +++ b/back/src/Model/User.ts @@ -1,11 +1,17 @@ import { Group } from "./Group"; import { PointInterface } from "./Websocket/PointInterface"; -import {Zone} from "_Model/Zone"; -import {Movable} from "_Model/Movable"; -import {PositionNotifier} from "_Model/PositionNotifier"; -import {ServerDuplexStream} from "grpc"; -import {BatchMessage, CompanionMessage, PusherToBackMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb"; -import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; +import { Zone } from "_Model/Zone"; +import { Movable } from "_Model/Movable"; +import { PositionNotifier } from "_Model/PositionNotifier"; +import { ServerDuplexStream } from "grpc"; +import { + BatchMessage, + CompanionMessage, + PusherToBackMessage, + ServerToClientMessage, + SubMessage, +} from "../Messages/generated/messages_pb"; +import { CharacterLayer } from "_Model/Websocket/CharacterLayer"; export type UserSocket = ServerDuplexStream; @@ -22,7 +28,7 @@ export class User implements Movable { private positionNotifier: PositionNotifier, public readonly socket: UserSocket, public readonly tags: string[], - public readonly visitCardUrl: string|null, + public readonly visitCardUrl: string | null, public readonly name: string, public readonly characterLayers: CharacterLayer[], public readonly companion?: CompanionMessage @@ -42,9 +48,8 @@ export class User implements Movable { this.positionNotifier.updatePosition(this, position, oldPosition); } - private batchedMessages: BatchMessage = new BatchMessage(); - private batchTimeout: NodeJS.Timeout|null = null; + private batchTimeout: NodeJS.Timeout | null = null; public emitInBatch(payload: SubMessage): void { this.batchedMessages.addPayload(payload); diff --git a/back/src/Model/Websocket/CharacterLayer.ts b/back/src/Model/Websocket/CharacterLayer.ts index 13d838ee..3e428790 100644 --- a/back/src/Model/Websocket/CharacterLayer.ts +++ b/back/src/Model/Websocket/CharacterLayer.ts @@ -1,4 +1,4 @@ export interface CharacterLayer { - name: string, - url: string|undefined + name: string; + url: string | undefined; } diff --git a/back/src/Model/Websocket/ItemEventMessage.ts b/back/src/Model/Websocket/ItemEventMessage.ts index b1f9203e..1bb7f615 100644 --- a/back/src/Model/Websocket/ItemEventMessage.ts +++ b/back/src/Model/Websocket/ItemEventMessage.ts @@ -1,10 +1,11 @@ import * as tg from "generic-type-guard"; -export const isItemEventMessageInterface = - new tg.IsInterface().withProperties({ +export const isItemEventMessageInterface = new tg.IsInterface() + .withProperties({ itemId: tg.isNumber, event: tg.isString, state: tg.isUnknown, parameters: tg.isUnknown, - }).get(); + }) + .get(); export type ItemEventMessageInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/MessageUserPosition.ts b/back/src/Model/Websocket/MessageUserPosition.ts index ee43d58c..19b57d2e 100644 --- a/back/src/Model/Websocket/MessageUserPosition.ts +++ b/back/src/Model/Websocket/MessageUserPosition.ts @@ -1,7 +1,10 @@ -import {PointInterface} from "./PointInterface"; +import { PointInterface } from "./PointInterface"; -export class Point implements PointInterface{ - constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) { - } +export class Point implements PointInterface { + constructor( + public x: number, + public y: number, + public direction: string = "none", + public moving: boolean = false + ) {} } - diff --git a/back/src/Model/Websocket/PointInterface.ts b/back/src/Model/Websocket/PointInterface.ts index afb07a23..d7c7826e 100644 --- a/back/src/Model/Websocket/PointInterface.ts +++ b/back/src/Model/Websocket/PointInterface.ts @@ -7,11 +7,12 @@ import * as tg from "generic-type-guard"; readonly moving: boolean; }*/ -export const isPointInterface = - new tg.IsInterface().withProperties({ +export const isPointInterface = new tg.IsInterface() + .withProperties({ x: tg.isNumber, y: tg.isNumber, direction: tg.isString, - moving: tg.isBoolean - }).get(); + moving: tg.isBoolean, + }) + .get(); export type PointInterface = tg.GuardedType; diff --git a/back/src/Model/Websocket/ProtobufUtils.ts b/back/src/Model/Websocket/ProtobufUtils.ts index b85a4257..68817a4f 100644 --- a/back/src/Model/Websocket/ProtobufUtils.ts +++ b/back/src/Model/Websocket/ProtobufUtils.ts @@ -1,34 +1,33 @@ -import {PointInterface} from "./PointInterface"; +import { PointInterface } from "./PointInterface"; import { CharacterLayerMessage, ItemEventMessage, PointMessage, - PositionMessage + PositionMessage, } from "../../Messages/generated/messages_pb"; -import {CharacterLayer} from "_Model/Websocket/CharacterLayer"; +import { CharacterLayer } from "_Model/Websocket/CharacterLayer"; import Direction = PositionMessage.Direction; -import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage"; -import {PositionInterface} from "_Model/PositionInterface"; +import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage"; +import { PositionInterface } from "_Model/PositionInterface"; export class ProtobufUtils { - public static toPositionMessage(point: PointInterface): PositionMessage { let direction: Direction; switch (point.direction) { - case 'up': + case "up": direction = Direction.UP; break; - case 'down': + case "down": direction = Direction.DOWN; break; - case 'left': + case "left": direction = Direction.LEFT; break; - case 'right': + case "right": direction = Direction.RIGHT; break; default: - throw new Error('unexpected direction'); + throw new Error("unexpected direction"); } const position = new PositionMessage(); @@ -44,16 +43,16 @@ export class ProtobufUtils { let direction: string; switch (position.getDirection()) { case Direction.UP: - direction = 'up'; + direction = "up"; break; case Direction.DOWN: - direction = 'down'; + direction = "down"; break; case Direction.LEFT: - direction = 'left'; + direction = "left"; break; case Direction.RIGHT: - direction = 'right'; + direction = "right"; break; default: throw new Error("Unexpected direction"); @@ -82,7 +81,7 @@ export class ProtobufUtils { event: itemEventMessage.getEvent(), parameters: JSON.parse(itemEventMessage.getParametersjson()), state: JSON.parse(itemEventMessage.getStatejson()), - } + }; } public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage { @@ -96,7 +95,7 @@ export class ProtobufUtils { } public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] { - return characterLayers.map(function(characterLayer): CharacterLayerMessage { + return characterLayers.map(function (characterLayer): CharacterLayerMessage { const message = new CharacterLayerMessage(); message.setName(characterLayer.name); if (characterLayer.url) { @@ -107,7 +106,7 @@ export class ProtobufUtils { } public static toCharacterLayerObjects(characterLayers: CharacterLayerMessage[]): CharacterLayer[] { - return characterLayers.map(function(characterLayer): CharacterLayer { + return characterLayers.map(function (characterLayer): CharacterLayer { const url = characterLayer.getUrl(); return { name: characterLayer.getName(), diff --git a/back/src/Model/Zone.ts b/back/src/Model/Zone.ts index ffb172bb..d236e489 100644 --- a/back/src/Model/Zone.ts +++ b/back/src/Model/Zone.ts @@ -1,35 +1,52 @@ -import {User} from "./User"; -import {PositionInterface} from "_Model/PositionInterface"; -import {Movable} from "./Movable"; -import {Group} from "./Group"; -import {ZoneSocket} from "../RoomManager"; -import {EmoteEventMessage} from "../Messages/generated/messages_pb"; +import { User } from "./User"; +import { PositionInterface } from "_Model/PositionInterface"; +import { Movable } from "./Movable"; +import { Group } from "./Group"; +import { ZoneSocket } from "../RoomManager"; +import { EmoteEventMessage } from "../Messages/generated/messages_pb"; -export type EntersCallback = (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => void; +export type EntersCallback = (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => void; export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void; -export type LeavesCallback = (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => void; +export type LeavesCallback = (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => void; export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void; export class Zone { private things: Set = new Set(); private listeners: Set = new Set(); - - - constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, private onEmote: EmoteCallback, public readonly x: number, public readonly y: number) { } + + constructor( + private onEnters: EntersCallback, + private onMoves: MovesCallback, + private onLeaves: LeavesCallback, + private onEmote: EmoteCallback, + public readonly x: number, + public readonly y: number + ) {} /** * A user/thing leaves the zone */ - public leave(thing: Movable, newZone: Zone|null) { + public leave(thing: Movable, newZone: Zone | null) { const result = this.things.delete(thing); if (!result) { if (thing instanceof User) { - throw new Error('Could not find user in zone '+thing.id); + throw new Error("Could not find user in zone " + thing.id); } if (thing instanceof Group) { - throw new Error('Could not find group '+thing.getId()+' in zone ('+this.x+','+this.y+'). Position of group: ('+thing.getPosition().x+','+thing.getPosition().y+')'); + throw new Error( + "Could not find group " + + thing.getId() + + " in zone (" + + this.x + + "," + + this.y + + "). Position of group: (" + + thing.getPosition().x + + "," + + thing.getPosition().y + + ")" + ); } - } this.notifyLeft(thing, newZone); } @@ -37,13 +54,13 @@ export class Zone { /** * Notify listeners of this zone that this user/thing left */ - private notifyLeft(thing: Movable, newZone: Zone|null) { + private notifyLeft(thing: Movable, newZone: Zone | null) { for (const listener of this.listeners) { this.onLeaves(thing, newZone, listener); } } - public enter(thing: Movable, oldZone: Zone|null, position: PositionInterface) { + public enter(thing: Movable, oldZone: Zone | null, position: PositionInterface) { this.things.add(thing); this.notifyEnter(thing, oldZone, position); } @@ -51,13 +68,12 @@ export class Zone { /** * Notify listeners of this zone that this user entered */ - private notifyEnter(thing: Movable, oldZone: Zone|null, position: PositionInterface) { + private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) { for (const listener of this.listeners) { this.onEnters(thing, oldZone, listener); } } - public move(thing: Movable, position: PositionInterface) { if (!this.things.has(thing)) { this.things.add(thing); @@ -67,7 +83,7 @@ export class Zone { for (const listener of this.listeners) { //if (listener !== thing) { - this.onMoves(thing,position, listener); + this.onMoves(thing, position, listener); //} } } @@ -89,6 +105,5 @@ export class Zone { for (const listener of this.listeners) { this.onEmote(emoteEventMessage, listener); } - } } diff --git a/back/src/RoomManager.ts b/back/src/RoomManager.ts index 19266687..9aaf1edb 100644 --- a/back/src/RoomManager.ts +++ b/back/src/RoomManager.ts @@ -1,4 +1,4 @@ -import {IRoomManagerServer} from "./Messages/generated/messages_grpc_pb"; +import { IRoomManagerServer } from "./Messages/generated/messages_grpc_pb"; import { AdminGlobalMessage, AdminMessage, @@ -11,92 +11,114 @@ import { JoinRoomMessage, PlayGlobalMessage, PusherToBackMessage, - QueryJitsiJwtMessage, RefreshRoomPromptMessage, + QueryJitsiJwtMessage, + RefreshRoomPromptMessage, ServerToAdminClientMessage, ServerToClientMessage, SilentMessage, UserMovesMessage, - WebRtcSignalToServerMessage, WorldFullWarningToRoomMessage, - ZoneMessage + WebRtcSignalToServerMessage, + WorldFullWarningToRoomMessage, + ZoneMessage, } from "./Messages/generated/messages_pb"; -import {sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream} from "grpc"; -import {socketManager} from "./Services/SocketManager"; -import {emitError} from "./Services/MessageHelpers"; -import {User, UserSocket} from "./Model/User"; -import {GameRoom} from "./Model/GameRoom"; +import { sendUnaryData, ServerDuplexStream, ServerUnaryCall, ServerWritableStream } from "grpc"; +import { socketManager } from "./Services/SocketManager"; +import { emitError } from "./Services/MessageHelpers"; +import { User, UserSocket } from "./Model/User"; +import { GameRoom } from "./Model/GameRoom"; import Debug from "debug"; -import {Admin} from "./Model/Admin"; +import { Admin } from "./Model/Admin"; -const debug = Debug('roommanager'); +const debug = Debug("roommanager"); export type AdminSocket = ServerDuplexStream; export type ZoneSocket = ServerWritableStream; const roomManager: IRoomManagerServer = { joinRoom: (call: UserSocket): void => { - console.log('joinRoom called'); + console.log("joinRoom called"); - let room: GameRoom|null = null; - let user: User|null = null; + let room: GameRoom | null = null; + let user: User | null = null; - call.on('data', (message: PusherToBackMessage) => { + call.on("data", (message: PusherToBackMessage) => { try { if (room === null || user === null) { if (message.hasJoinroommessage()) { - socketManager.handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage).then(({room: gameRoom, user: myUser}) => { - if (call.writable) { - room = gameRoom; - user = myUser; - } else { - //Connexion may have been closed before the init was finished, so we have to manually disconnect the user. - socketManager.leaveRoom(gameRoom, myUser); - } - }); + socketManager + .handleJoinRoom(call, message.getJoinroommessage() as JoinRoomMessage) + .then(({ room: gameRoom, user: myUser }) => { + if (call.writable) { + room = gameRoom; + user = myUser; + } else { + //Connexion may have been closed before the init was finished, so we have to manually disconnect the user. + socketManager.leaveRoom(gameRoom, myUser); + } + }); } else { - throw new Error('The first message sent MUST be of type JoinRoomMessage'); + throw new Error("The first message sent MUST be of type JoinRoomMessage"); } } else { if (message.hasJoinroommessage()) { - throw new Error('Cannot call JoinRoomMessage twice!'); + throw new Error("Cannot call JoinRoomMessage twice!"); } else if (message.hasUsermovesmessage()) { - socketManager.handleUserMovesMessage(room, user, message.getUsermovesmessage() as UserMovesMessage); + socketManager.handleUserMovesMessage( + room, + user, + message.getUsermovesmessage() as UserMovesMessage + ); } else if (message.hasSilentmessage()) { socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage); } else if (message.hasItemeventmessage()) { socketManager.handleItemEvent(room, user, message.getItemeventmessage() as ItemEventMessage); } else if (message.hasWebrtcsignaltoservermessage()) { - socketManager.emitVideo(room, user, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitVideo( + room, + user, + message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage + ); } else if (message.hasWebrtcscreensharingsignaltoservermessage()) { - socketManager.emitScreenSharing(room, user, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitScreenSharing( + room, + user, + message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage + ); } else if (message.hasPlayglobalmessage()) { socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage); - } else if (message.hasQueryjitsijwtmessage()){ - socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); - } else if (message.hasEmotepromptmessage()){ - socketManager.handleEmoteEventMessage(room, user, message.getEmotepromptmessage() as EmotePromptMessage); - }else if (message.hasSendusermessage()) { + } else if (message.hasQueryjitsijwtmessage()) { + socketManager.handleQueryJitsiJwtMessage( + user, + message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage + ); + } else if (message.hasEmotepromptmessage()) { + socketManager.handleEmoteEventMessage( + room, + user, + message.getEmotepromptmessage() as EmotePromptMessage + ); + } else if (message.hasSendusermessage()) { const sendUserMessage = message.getSendusermessage(); - if(sendUserMessage !== undefined) { + if (sendUserMessage !== undefined) { socketManager.handlerSendUserMessage(user, sendUserMessage); } - }else if (message.hasBanusermessage()) { + } else if (message.hasBanusermessage()) { const banUserMessage = message.getBanusermessage(); - if(banUserMessage !== undefined) { + if (banUserMessage !== undefined) { socketManager.handlerBanUserMessage(room, user, banUserMessage); } } else { - throw new Error('Unhandled message type'); + throw new Error("Unhandled message type"); } } } catch (e) { emitError(call, e); call.end(); } - }); - call.on('end', () => { - debug('joinRoom ended'); + call.on("end", () => { + debug("joinRoom ended"); if (user !== null && room !== null) { socketManager.leaveRoom(room, user); } @@ -105,41 +127,40 @@ const roomManager: IRoomManagerServer = { user = null; }); - call.on('error', (err: Error) => { - console.error('An error occurred in joinRoom stream:', err); + call.on("error", (err: Error) => { + console.error("An error occurred in joinRoom stream:", err); }); - }, listenZone(call: ZoneSocket): void { - debug('listenZone called'); + debug("listenZone called"); const zoneMessage = call.request; socketManager.addZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); - call.on('cancelled', () => { - debug('listenZone cancelled'); + call.on("cancelled", () => { + debug("listenZone cancelled"); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); call.end(); - }) - - call.on('close', () => { - debug('listenZone connection closed'); + }); + + call.on("close", () => { + debug("listenZone connection closed"); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); - }).on('error', (e) => { - console.error('An error occurred in listenZone stream:', e); + }).on("error", (e) => { + console.error("An error occurred in listenZone stream:", e); socketManager.removeZoneListener(call, zoneMessage.getRoomid(), zoneMessage.getX(), zoneMessage.getY()); call.end(); }); }, adminRoom(call: AdminSocket): void { - console.log('adminRoom called'); + console.log("adminRoom called"); const admin = new Admin(call); - let room: GameRoom|null = null; + let room: GameRoom | null = null; - call.on('data', (message: AdminPusherToBackMessage) => { + call.on("data", (message: AdminPusherToBackMessage) => { try { if (room === null) { if (message.hasSubscribetoroom()) { @@ -148,18 +169,17 @@ const roomManager: IRoomManagerServer = { room = gameRoom; }); } else { - throw new Error('The first message sent MUST be of type JoinRoomMessage'); + throw new Error("The first message sent MUST be of type JoinRoomMessage"); } } } catch (e) { emitError(call, e); call.end(); } - }); - call.on('end', () => { - debug('joinRoom ended'); + call.on("end", () => { + debug("joinRoom ended"); if (room !== null) { socketManager.leaveAdminRoom(room, admin); } @@ -167,18 +187,21 @@ const roomManager: IRoomManagerServer = { room = null; }); - call.on('error', (err: Error) => { - console.error('An error occurred in joinAdminRoom stream:', err); + call.on("error", (err: Error) => { + console.error("An error occurred in joinAdminRoom stream:", err); }); }, sendAdminMessage(call: ServerUnaryCall, callback: sendUnaryData): void { - - socketManager.sendAdminMessage(call.request.getRoomid(), call.request.getRecipientuuid(), call.request.getMessage()); + socketManager.sendAdminMessage( + call.request.getRoomid(), + call.request.getRecipientuuid(), + call.request.getMessage() + ); callback(null, new EmptyMessage()); }, sendGlobalAdminMessage(call: ServerUnaryCall, callback: sendUnaryData): void { - throw new Error('Not implemented yet'); + throw new Error("Not implemented yet"); // TODO callback(null, new EmptyMessage()); }, @@ -192,14 +215,20 @@ const roomManager: IRoomManagerServer = { socketManager.sendAdminRoomMessage(call.request.getRoomid(), call.request.getMessage()); callback(null, new EmptyMessage()); }, - sendWorldFullWarningToRoom(call: ServerUnaryCall, callback: sendUnaryData): void { + sendWorldFullWarningToRoom( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { socketManager.dispatchWorlFullWarning(call.request.getRoomid()); callback(null, new EmptyMessage()); }, - sendRefreshRoomPrompt(call: ServerUnaryCall, callback: sendUnaryData): void { + sendRefreshRoomPrompt( + call: ServerUnaryCall, + callback: sendUnaryData + ): void { socketManager.dispatchRoomRefresh(call.request.getRoomid()); callback(null, new EmptyMessage()); }, }; -export {roomManager}; +export { roomManager }; diff --git a/back/src/Server/server/app.ts b/back/src/Server/server/app.ts index 3b98a9b3..4c422d5c 100644 --- a/back/src/Server/server/app.ts +++ b/back/src/Server/server/app.ts @@ -1,13 +1,13 @@ -import { App as _App, AppOptions } from 'uWebSockets.js'; -import BaseApp from './baseapp'; -import { extend } from './utils'; -import { UwsApp } from './types'; +import { App as _App, AppOptions } from "uWebSockets.js"; +import BaseApp from "./baseapp"; +import { extend } from "./utils"; +import { UwsApp } from "./types"; class App extends (_App) { - constructor(options: AppOptions = {}) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions = {}) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default App; diff --git a/back/src/Server/server/baseapp.ts b/back/src/Server/server/baseapp.ts index accd8a99..6d973ac7 100644 --- a/back/src/Server/server/baseapp.ts +++ b/back/src/Server/server/baseapp.ts @@ -1,116 +1,109 @@ -import { Readable } from 'stream'; -import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; +import { Readable } from "stream"; +import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; -import formData from './formdata'; -import { stob } from './utils'; -import { Handler } from './types'; -import {join} from "path"; +import formData from "./formdata"; +import { stob } from "./utils"; +import { Handler } from "./types"; +import { join } from "path"; -const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data']; +const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"]; const noOp = () => true; const handleBody = (res: HttpResponse, req: HttpRequest) => { - const contType = req.getHeader('content-type'); + const contType = req.getHeader("content-type"); - res.bodyStream = function() { - const stream = new Readable(); - stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method + res.bodyStream = function () { + const stream = new Readable(); + stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method - this.onData((ab: ArrayBuffer, isLast: boolean) => { - // uint and then slicing is bit faster than slice and then uint - stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any - if (isLast) { - stream.push(null); - } - }); + this.onData((ab: ArrayBuffer, isLast: boolean) => { + // uint and then slicing is bit faster than slice and then uint + stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any + if (isLast) { + stream.push(null); + } + }); - return stream; - }; + return stream; + }; - res.body = () => stob(res.bodyStream()); + res.body = () => stob(res.bodyStream()); - if (contType.includes('application/json')) - res.json = async () => JSON.parse(await res.body()); - if (contTypes.map(t => contType.includes(t)).includes(true)) - res.formData = formData.bind(res, contType); + if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body()); + if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType); }; class BaseApp { - _sockets = new Map(); - ws!: TemplatedApp['ws']; - get!: TemplatedApp['get']; - _post!: TemplatedApp['post']; - _put!: TemplatedApp['put']; - _patch!: TemplatedApp['patch']; - _listen!: TemplatedApp['listen']; + _sockets = new Map(); + ws!: TemplatedApp["ws"]; + get!: TemplatedApp["get"]; + _post!: TemplatedApp["post"]; + _put!: TemplatedApp["put"]; + _patch!: TemplatedApp["patch"]; + _listen!: TemplatedApp["listen"]; - post(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._post(pattern, (res, req) => { - handleBody(res, req); - handler(res, req); - }); - return this; - } + post(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._post(pattern, (res, req) => { + handleBody(res, req); + handler(res, req); + }); + return this; + } - put(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._put(pattern, (res, req) => { - handleBody(res, req); + put(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._put(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - patch(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._patch(pattern, (res, req) => { - handleBody(res, req); + patch(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._patch(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - listen(h: string | number, p: Function | number = noOp, cb?: Function) { - if (typeof p === 'number' && typeof h === 'string') { - this._listen(h, p, socket => { - this._sockets.set(p, socket); - if (cb === undefined) { - throw new Error('cb undefined'); + listen(h: string | number, p: Function | number = noOp, cb?: Function) { + if (typeof p === "number" && typeof h === "string") { + this._listen(h, p, (socket) => { + this._sockets.set(p, socket); + if (cb === undefined) { + throw new Error("cb undefined"); + } + cb(socket); + }); + } else if (typeof h === "number" && typeof p === "function") { + this._listen(h, (socket) => { + this._sockets.set(h, socket); + p(socket); + }); + } else { + throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)"); } - cb(socket); - }); - } else if (typeof h === 'number' && typeof p === 'function') { - this._listen(h, socket => { - this._sockets.set(h, socket); - p(socket); - }); - } else { - throw Error( - 'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)' - ); + + return this; } - return this; - } - - close(port: null | number = null) { - if (port) { - this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); - this._sockets.delete(port); - } else { - this._sockets.forEach(app => { - us_listen_socket_close(app); - }); - this._sockets.clear(); + close(port: null | number = null) { + if (port) { + this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); + this._sockets.delete(port); + } else { + this._sockets.forEach((app) => { + us_listen_socket_close(app); + }); + this._sockets.clear(); + } + return this; } - return this; - } } export default BaseApp; diff --git a/back/src/Server/server/formdata.ts b/back/src/Server/server/formdata.ts index 9dd08440..66e51db4 100644 --- a/back/src/Server/server/formdata.ts +++ b/back/src/Server/server/formdata.ts @@ -1,100 +1,99 @@ -import { createWriteStream } from 'fs'; -import { join, dirname } from 'path'; -import Busboy from 'busboy'; -import mkdirp from 'mkdirp'; +import { createWriteStream } from "fs"; +import { join, dirname } from "path"; +import Busboy from "busboy"; +import mkdirp from "mkdirp"; function formData( - contType: string, - options: busboy.BusboyConfig & { - abortOnLimit?: boolean; - tmpDir?: string; - onFile?: ( - fieldname: string, - file: NodeJS.ReadableStream, - filename: string, - encoding: string, - mimetype: string - ) => string; - onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any - filename?: (oldName: string) => string; - } = {} + contType: string, + options: busboy.BusboyConfig & { + abortOnLimit?: boolean; + tmpDir?: string; + onFile?: ( + fieldname: string, + file: NodeJS.ReadableStream, + filename: string, + encoding: string, + mimetype: string + ) => string; + onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + filename?: (oldName: string) => string; + } = {} ) { - console.log('Enter form data'); - options.headers = { - 'content-type': contType - }; + console.log("Enter form data"); + options.headers = { + "content-type": contType, + }; - return new Promise((resolve, reject) => { - const busb = new Busboy(options); - const ret = {}; + return new Promise((resolve, reject) => { + const busb = new Busboy(options); + const ret = {}; - this.bodyStream().pipe(busb); + this.bodyStream().pipe(busb); - busb.on('limit', () => { - if (options.abortOnLimit) { - reject(Error('limit')); - } + busb.on("limit", () => { + if (options.abortOnLimit) { + reject(Error("limit")); + } + }); + + busb.on("file", function (fieldname, file, filename, encoding, mimetype) { + const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = { + filename, + encoding, + mimetype, + filePath: undefined, + }; + + if (typeof options.tmpDir === "string") { + if (typeof options.filename === "function") filename = options.filename(filename); + const fileToSave = join(options.tmpDir, filename); + mkdirp(dirname(fileToSave)); + + file.pipe(createWriteStream(fileToSave)); + value.filePath = fileToSave; + } + if (typeof options.onFile === "function") { + value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath; + } + + setRetValue(ret, fieldname, value); + }); + + busb.on("field", function (fieldname, value) { + if (typeof options.onField === "function") options.onField(fieldname, value); + + setRetValue(ret, fieldname, value); + }); + + busb.on("finish", function () { + resolve(ret); + }); + + busb.on("error", reject); }); - - busb.on('file', function(fieldname, file, filename, encoding, mimetype) { - const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = { - filename, - encoding, - mimetype, - filePath: undefined - }; - - if (typeof options.tmpDir === 'string') { - if (typeof options.filename === 'function') filename = options.filename(filename); - const fileToSave = join(options.tmpDir, filename); - mkdirp(dirname(fileToSave)); - - file.pipe(createWriteStream(fileToSave)); - value.filePath = fileToSave; - } - if (typeof options.onFile === 'function') { - value.filePath = - options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath; - } - - setRetValue(ret, fieldname, value); - }); - - busb.on('field', function(fieldname, value) { - if (typeof options.onField === 'function') options.onField(fieldname, value); - - setRetValue(ret, fieldname, value); - }); - - busb.on('finish', function() { - resolve(ret); - }); - - busb.on('error', reject); - }); } function setRetValue( - ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any - fieldname: string, - value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any + ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any + fieldname: string, + value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any ) { - if (fieldname.endsWith('[]')) { - fieldname = fieldname.slice(0, fieldname.length - 2); - if (Array.isArray(ret[fieldname])) { - ret[fieldname].push(value); + if (fieldname.endsWith("[]")) { + fieldname = fieldname.slice(0, fieldname.length - 2); + if (Array.isArray(ret[fieldname])) { + ret[fieldname].push(value); + } else { + ret[fieldname] = [value]; + } } else { - ret[fieldname] = [value]; + if (Array.isArray(ret[fieldname])) { + ret[fieldname].push(value); + } else if (ret[fieldname]) { + ret[fieldname] = [ret[fieldname], value]; + } else { + ret[fieldname] = value; + } } - } else { - if (Array.isArray(ret[fieldname])) { - ret[fieldname].push(value); - } else if (ret[fieldname]) { - ret[fieldname] = [ret[fieldname], value]; - } else { - ret[fieldname] = value; - } - } } export default formData; diff --git a/back/src/Server/server/sslapp.ts b/back/src/Server/server/sslapp.ts index 46ae89a5..80df0e4a 100644 --- a/back/src/Server/server/sslapp.ts +++ b/back/src/Server/server/sslapp.ts @@ -1,13 +1,13 @@ -import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js'; -import BaseApp from './baseapp'; -import { extend } from './utils'; -import { UwsApp } from './types'; +import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js"; +import BaseApp from "./baseapp"; +import { extend } from "./utils"; +import { UwsApp } from "./types"; class SSLApp extends (_SSLApp) { - constructor(options: AppOptions) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default SSLApp; diff --git a/back/src/Server/server/types.ts b/back/src/Server/server/types.ts index 3d0f48c7..afc21d17 100644 --- a/back/src/Server/server/types.ts +++ b/back/src/Server/server/types.ts @@ -1,9 +1,9 @@ -import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; +import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; export type UwsApp = { - (options: AppOptions): TemplatedApp; - new (options: AppOptions): TemplatedApp; - prototype: TemplatedApp; + (options: AppOptions): TemplatedApp; + new (options: AppOptions): TemplatedApp; + prototype: TemplatedApp; }; export type Handler = (res: HttpResponse, req: HttpRequest) => void; diff --git a/back/src/Server/server/utils.ts b/back/src/Server/server/utils.ts index 80ea3938..dc813064 100644 --- a/back/src/Server/server/utils.ts +++ b/back/src/Server/server/utils.ts @@ -1,37 +1,36 @@ -import { ReadStream } from 'fs'; +import { ReadStream } from "fs"; -function extend(who: any, from: any, overwrite = true) { // eslint-disable-line @typescript-eslint/no-explicit-any - const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat( - Object.keys(from) - ); - ownProps.forEach(prop => { - if (prop === 'constructor' || from[prop] === undefined) return; - if (who[prop] && overwrite) { - who[`_${prop}`] = who[prop]; - } - if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who); - else who[prop] = from[prop]; - }); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function extend(who: any, from: any, overwrite = true) { + const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from)); + ownProps.forEach((prop) => { + if (prop === "constructor" || from[prop] === undefined) return; + if (who[prop] && overwrite) { + who[`_${prop}`] = who[prop]; + } + if (typeof from[prop] === "function") who[prop] = from[prop].bind(who); + else who[prop] = from[prop]; + }); } function stob(stream: ReadStream): Promise { - return new Promise(resolve => { - const buffers: Buffer[] = []; - stream.on('data', buffers.push.bind(buffers)); + return new Promise((resolve) => { + const buffers: Buffer[] = []; + stream.on("data", buffers.push.bind(buffers)); - stream.on('end', () => { - switch (buffers.length) { - case 0: - resolve(Buffer.allocUnsafe(0)); - break; - case 1: - resolve(buffers[0]); - break; - default: - resolve(Buffer.concat(buffers)); - } + stream.on("end", () => { + switch (buffers.length) { + case 0: + resolve(Buffer.allocUnsafe(0)); + break; + case 1: + resolve(buffers[0]); + break; + default: + resolve(Buffer.concat(buffers)); + } + }); }); - }); } export { extend, stob }; diff --git a/back/src/Server/sifrr.server.ts b/back/src/Server/sifrr.server.ts index 47fba02c..4ef03721 100644 --- a/back/src/Server/sifrr.server.ts +++ b/back/src/Server/sifrr.server.ts @@ -1,19 +1,19 @@ -import { parse } from 'query-string'; -import { HttpRequest } from 'uWebSockets.js'; -import App from './server/app'; -import SSLApp from './server/sslapp'; -import * as types from './server/types'; +import { parse } from "query-string"; +import { HttpRequest } from "uWebSockets.js"; +import App from "./server/app"; +import SSLApp from "./server/sslapp"; +import * as types from "./server/types"; const getQuery = (req: HttpRequest) => { - return parse(req.getQuery()); + return parse(req.getQuery()); }; export { App, SSLApp, getQuery }; -export * from './server/types'; +export * from "./server/types"; export default { - App, - SSLApp, - getQuery, - ...types + App, + SSLApp, + getQuery, + ...types, }; diff --git a/back/src/Services/ArrayHelper.ts b/back/src/Services/ArrayHelper.ts index 67321d1b..8af1da9f 100644 --- a/back/src/Services/ArrayHelper.ts +++ b/back/src/Services/ArrayHelper.ts @@ -1,3 +1,3 @@ -export const arrayIntersect = (array1: string[], array2: string[]) : boolean => { - return array1.filter(value => array2.includes(value)).length > 0; -} \ No newline at end of file +export const arrayIntersect = (array1: string[], array2: string[]): boolean => { + return array1.filter((value) => array2.includes(value)).length > 0; +}; diff --git a/back/src/Services/ClientEventsEmitter.ts b/back/src/Services/ClientEventsEmitter.ts index 381137a1..0f56d55c 100644 --- a/back/src/Services/ClientEventsEmitter.ts +++ b/back/src/Services/ClientEventsEmitter.ts @@ -1,7 +1,7 @@ -const EventEmitter = require('events'); +const EventEmitter = require("events"); -const clientJoinEvent = 'clientJoin'; -const clientLeaveEvent = 'clientLeave'; +const clientJoinEvent = "clientJoin"; +const clientLeaveEvent = "clientLeave"; class ClientEventsEmitter extends EventEmitter { emitClientJoin(clientUUid: string, roomId: string): void { diff --git a/back/src/Services/CpuTracker.ts b/back/src/Services/CpuTracker.ts index c7d57f3d..3d06ca70 100644 --- a/back/src/Services/CpuTracker.ts +++ b/back/src/Services/CpuTracker.ts @@ -1,6 +1,6 @@ -import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable"; +import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable"; -function secNSec2ms(secNSec: Array|number) { +function secNSec2ms(secNSec: Array | number) { if (Array.isArray(secNSec)) { return secNSec[0] * 1000 + secNSec[1] / 1000000; } @@ -12,17 +12,17 @@ class CpuTracker { private overHeating: boolean = false; constructor() { - let time = process.hrtime.bigint() - let usage = process.cpuUsage() + let time = process.hrtime.bigint(); + let usage = process.cpuUsage(); setInterval(() => { const elapTime = process.hrtime.bigint(); - const elapUsage = process.cpuUsage(usage) - usage = process.cpuUsage() + const elapUsage = process.cpuUsage(usage); + usage = process.cpuUsage(); const elapTimeMS = elapTime - time; - const elapUserMS = secNSec2ms(elapUsage.user) - const elapSystMS = secNSec2ms(elapUsage.system) - this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000) + const elapUserMS = secNSec2ms(elapUsage.user); + const elapSystMS = secNSec2ms(elapUsage.system); + this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000); time = elapTime; diff --git a/back/src/Services/GaugeManager.ts b/back/src/Services/GaugeManager.ts index 80712856..6d2183d8 100644 --- a/back/src/Services/GaugeManager.ts +++ b/back/src/Services/GaugeManager.ts @@ -1,4 +1,4 @@ -import {Counter, Gauge} from "prom-client"; +import { Counter, Gauge } from "prom-client"; //this class should manage all the custom metrics used by prometheus class GaugeManager { @@ -10,29 +10,29 @@ class GaugeManager { constructor() { this.nbRoomsGauge = new Gauge({ - name: 'workadventure_nb_rooms', - help: 'Number of active rooms' + name: "workadventure_nb_rooms", + help: "Number of active rooms", }); this.nbClientsGauge = new Gauge({ - name: 'workadventure_nb_sockets', - help: 'Number of connected sockets', - labelNames: [ ] + name: "workadventure_nb_sockets", + help: "Number of connected sockets", + labelNames: [], }); this.nbClientsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_clients_per_room', - help: 'Number of clients per room', - labelNames: [ 'room' ] + name: "workadventure_nb_clients_per_room", + help: "Number of clients per room", + labelNames: ["room"], }); this.nbGroupsPerRoomCounter = new Counter({ - name: 'workadventure_counter_groups_per_room', - help: 'Counter of groups per room', - labelNames: [ 'room' ] + name: "workadventure_counter_groups_per_room", + help: "Counter of groups per room", + labelNames: ["room"], }); this.nbGroupsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_groups_per_room', - help: 'Number of groups per room', - labelNames: [ 'room' ] + name: "workadventure_nb_groups_per_room", + help: "Number of groups per room", + labelNames: ["room"], }); } @@ -54,13 +54,13 @@ class GaugeManager { } incNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomCounter.inc({ room: roomId }) - this.nbGroupsPerRoomGauge.inc({ room: roomId }) + this.nbGroupsPerRoomCounter.inc({ room: roomId }); + this.nbGroupsPerRoomGauge.inc({ room: roomId }); } - + decNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomGauge.dec({ room: roomId }) + this.nbGroupsPerRoomGauge.dec({ room: roomId }); } } -export const gaugeManager = new GaugeManager(); \ No newline at end of file +export const gaugeManager = new GaugeManager(); diff --git a/back/src/Services/MessageHelpers.ts b/back/src/Services/MessageHelpers.ts index b2600a4a..493f7173 100644 --- a/back/src/Services/MessageHelpers.ts +++ b/back/src/Services/MessageHelpers.ts @@ -1,5 +1,5 @@ -import {ErrorMessage, ServerToClientMessage} from "../Messages/generated/messages_pb"; -import {UserSocket} from "_Model/User"; +import { ErrorMessage, ServerToClientMessage } from "../Messages/generated/messages_pb"; +import { UserSocket } from "_Model/User"; export function emitError(Client: UserSocket, message: string): void { const errorMessage = new ErrorMessage(); @@ -9,7 +9,7 @@ export function emitError(Client: UserSocket, message: string): void { serverToClientMessage.setErrormessage(errorMessage); //if (!Client.disconnecting) { - Client.write(serverToClientMessage); + Client.write(serverToClientMessage); //} console.warn(message); } diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts index a56a1ac4..e61763cd 100644 --- a/back/src/Services/SocketManager.ts +++ b/back/src/Services/SocketManager.ts @@ -1,4 +1,4 @@ -import {GameRoom} from "../Model/GameRoom"; +import { GameRoom } from "../Model/GameRoom"; import { ItemEventMessage, ItemStateMessage, @@ -27,39 +27,39 @@ import { WorldFullWarningMessage, UserLeftZoneMessage, EmoteEventMessage, - BanUserMessage, RefreshRoomMessage, EmotePromptMessage, + BanUserMessage, + RefreshRoomMessage, + EmotePromptMessage, } from "../Messages/generated/messages_pb"; -import {User, UserSocket} from "../Model/User"; -import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {Group} from "../Model/Group"; -import {cpuTracker} from "./CpuTracker"; +import { User, UserSocket } from "../Model/User"; +import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; +import { Group } from "../Model/Group"; +import { cpuTracker } from "./CpuTracker"; import { GROUP_RADIUS, JITSI_ISS, MINIMUM_DISTANCE, SECRET_JITSI_KEY, - TURN_STATIC_AUTH_SECRET + TURN_STATIC_AUTH_SECRET, } from "../Enum/EnvironmentVariable"; -import {Movable} from "../Model/Movable"; -import {PositionInterface} from "../Model/PositionInterface"; +import { Movable } from "../Model/Movable"; +import { PositionInterface } from "../Model/PositionInterface"; import Jwt from "jsonwebtoken"; -import {JITSI_URL} from "../Enum/EnvironmentVariable"; -import {clientEventsEmitter} from "./ClientEventsEmitter"; -import {gaugeManager} from "./GaugeManager"; -import {ZoneSocket} from "../RoomManager"; -import {Zone} from "_Model/Zone"; +import { JITSI_URL } from "../Enum/EnvironmentVariable"; +import { clientEventsEmitter } from "./ClientEventsEmitter"; +import { gaugeManager } from "./GaugeManager"; +import { ZoneSocket } from "../RoomManager"; +import { Zone } from "_Model/Zone"; import Debug from "debug"; -import {Admin} from "_Model/Admin"; +import { Admin } from "_Model/Admin"; import crypto from "crypto"; - -const debug = Debug('sockermanager'); +const debug = Debug("sockermanager"); function emitZoneMessage(subMessage: SubToPusherMessage, socket: ZoneSocket): void { // TODO: should we batch those every 100ms? const batchMessage = new BatchToPusherMessage(); batchMessage.addPayload(subMessage); - socket.write(batchMessage); } @@ -68,7 +68,6 @@ export class SocketManager { private rooms: Map = new Map(); constructor() { - clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => { gaugeManager.incNbClientPerRoomGauge(roomId); }); @@ -77,16 +76,18 @@ export class SocketManager { }); } - public async handleJoinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> { - + public async handleJoinRoom( + socket: UserSocket, + joinRoomMessage: JoinRoomMessage + ): Promise<{ room: GameRoom; user: User }> { //join new previous room - const {room, user} = await this.joinRoom(socket, joinRoomMessage); - + const { room, user } = await this.joinRoom(socket, joinRoomMessage); + if (!socket.writable) { - console.warn('Socket was aborted'); + console.warn("Socket was aborted"); return { room, - user + user, }; } const roomJoinedMessage = new RoomJoinedMessage(); @@ -108,9 +109,8 @@ export class SocketManager { return { room, - user + user, }; - } handleUserMovesMessage(room: GameRoom, user: User, userMovesMessage: UserMovesMessage) { @@ -124,13 +124,12 @@ export class SocketManager { } if (position === undefined) { - throw new Error('Position not found in message'); + throw new Error("Position not found in message"); } const viewport = userMoves.viewport; if (viewport === undefined) { - throw new Error('Viewport not found in message'); + throw new Error("Viewport not found in message"); } - // update position in the world room.updatePosition(user, ProtobufUtils.toPointInterface(position)); @@ -189,7 +188,11 @@ export class SocketManager { //send only at user const remoteUser = room.getUsers().get(data.getReceiverid()); if (remoteUser === undefined) { - console.warn("While exchanging a WebRTC signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); + console.warn( + "While exchanging a WebRTC signal: client with id ", + data.getReceiverid(), + " does not exist. This might be a race condition." + ); return; } @@ -197,8 +200,8 @@ export class SocketManager { webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setSignal(data.getSignal()); // TODO: only compute credentials if data.signal.type === "offer" - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); webrtcSignalToClient.setWebrtcusername(username); webrtcSignalToClient.setWebrtcpassword(password); } @@ -207,7 +210,7 @@ export class SocketManager { serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient); //if (!client.disconnecting) { - remoteUser.socket.write(serverToClientMessage); + remoteUser.socket.write(serverToClientMessage); //} } @@ -215,7 +218,11 @@ export class SocketManager { //send only at user const remoteUser = room.getUsers().get(data.getReceiverid()); if (remoteUser === undefined) { - console.warn("While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); + console.warn( + "While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", + data.getReceiverid(), + " does not exist. This might be a race condition." + ); return; } @@ -223,8 +230,8 @@ export class SocketManager { webrtcSignalToClient.setUserid(user.id); webrtcSignalToClient.setSignal(data.getSignal()); // TODO: only compute credentials if data.signal.type === "offer" - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); webrtcSignalToClient.setWebrtcusername(username); webrtcSignalToClient.setWebrtcpassword(password); } @@ -233,11 +240,11 @@ export class SocketManager { serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient); //if (!client.disconnecting) { - remoteUser.socket.write(serverToClientMessage); + remoteUser.socket.write(serverToClientMessage); //} } - leaveRoom(room: GameRoom, user: User){ + leaveRoom(room: GameRoom, user: User) { // leave previous room and world try { //user leave previous world @@ -249,33 +256,39 @@ export class SocketManager { } } finally { clientEventsEmitter.emitClientLeave(user.uuid, room.roomId); - console.log('A user left'); + console.log("A user left"); } } async getOrCreateRoom(roomId: string): Promise { //check and create new world for a room - let world = this.rooms.get(roomId) - if(world === undefined){ + let world = this.rooms.get(roomId); + if (world === undefined) { world = new GameRoom( roomId, (user: User, group: Group) => this.joinWebRtcRoom(user, group), (user: User, group: Group) => this.disConnectedUser(user, group), MINIMUM_DISTANCE, GROUP_RADIUS, - (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => this.onZoneEnter(thing, fromZone, listener), - (thing: Movable, position:PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener), - (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener), - (emoteEventMessage:EmoteEventMessage, listener: ZoneSocket) => this.onEmote(emoteEventMessage, listener), + (thing: Movable, fromZone: Zone | null, listener: ZoneSocket) => + this.onZoneEnter(thing, fromZone, listener), + (thing: Movable, position: PositionInterface, listener: ZoneSocket) => + this.onClientMove(thing, position, listener), + (thing: Movable, newZone: Zone | null, listener: ZoneSocket) => + this.onClientLeave(thing, newZone, listener), + (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => + this.onEmote(emoteEventMessage, listener) ); gaugeManager.incNbRoomGauge(); this.rooms.set(roomId, world); } - return Promise.resolve(world) + return Promise.resolve(world); } - private async joinRoom(socket: UserSocket, joinRoomMessage: JoinRoomMessage): Promise<{ room: GameRoom; user: User }> { - + private async joinRoom( + socket: UserSocket, + joinRoomMessage: JoinRoomMessage + ): Promise<{ room: GameRoom; user: User }> { const roomId = joinRoomMessage.getRoomid(); const room = await socketManager.getOrCreateRoom(roomId); @@ -284,15 +297,15 @@ export class SocketManager { const user = room.join(socket, joinRoomMessage); clientEventsEmitter.emitClientJoin(user.uuid, roomId); - console.log(new Date().toISOString() + ' A user joined'); - return {room, user}; + console.log(new Date().toISOString() + " A user joined"); + return { room, user }; } - private onZoneEnter(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) { + private onZoneEnter(thing: Movable, fromZone: Zone | null, listener: ZoneSocket) { if (thing instanceof User) { const userJoinedZoneMessage = new UserJoinedZoneMessage(); if (!Number.isInteger(thing.id)) { - throw new Error('clientUser.userId is not an integer '+thing.id); + throw new Error("clientUser.userId is not an integer " + thing.id); } userJoinedZoneMessage.setUserid(thing.id); userJoinedZoneMessage.setName(thing.name); @@ -312,11 +325,11 @@ export class SocketManager { } else if (thing instanceof Group) { this.emitCreateUpdateGroupEvent(listener, fromZone, thing); } else { - console.error('Unexpected type for Movable.'); + console.error("Unexpected type for Movable."); } } - private onClientMove(thing: Movable, position:PositionInterface, listener: ZoneSocket): void { + private onClientMove(thing: Movable, position: PositionInterface, listener: ZoneSocket): void { if (thing instanceof User) { const userMovedMessage = new UserMovedMessage(); userMovedMessage.setUserid(thing.id); @@ -331,21 +344,20 @@ export class SocketManager { } else if (thing instanceof Group) { this.emitCreateUpdateGroupEvent(listener, null, thing); } else { - console.error('Unexpected type for Movable.'); + console.error("Unexpected type for Movable."); } } - private onClientLeave(thing: Movable, newZone: Zone|null, listener: ZoneSocket) { + private onClientLeave(thing: Movable, newZone: Zone | null, listener: ZoneSocket) { if (thing instanceof User) { this.emitUserLeftEvent(listener, thing.id, newZone); } else if (thing instanceof Group) { this.emitDeleteGroupEvent(listener, thing.getId(), newZone); } else { - console.error('Unexpected type for Movable.'); + console.error("Unexpected type for Movable."); } } - private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) { const subMessage = new SubToPusherMessage(); subMessage.setEmoteeventmessage(emoteEventMessage); @@ -353,7 +365,7 @@ export class SocketManager { emitZoneMessage(subMessage, client); } - private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone|null, group: Group): void { + private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone | null, group: Group): void { const position = group.getPosition(); const pointMessage = new PointMessage(); pointMessage.setX(Math.floor(position.x)); @@ -371,7 +383,7 @@ export class SocketManager { //client.emitInBatch(subMessage); } - private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone|null): void { + private emitDeleteGroupEvent(client: ZoneSocket, groupId: number, newZone: Zone | null): void { const groupDeleteMessage = new GroupLeftZoneMessage(); groupDeleteMessage.setGroupid(groupId); groupDeleteMessage.setTozone(this.toProtoZone(newZone)); @@ -383,7 +395,7 @@ export class SocketManager { //user.emitInBatch(subMessage); } - private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone|null): void { + private emitUserLeftEvent(client: ZoneSocket, userId: number, newZone: Zone | null): void { const userLeftMessage = new UserLeftZoneMessage(); userLeftMessage.setUserid(userId); userLeftMessage.setTozone(this.toProtoZone(newZone)); @@ -394,7 +406,7 @@ export class SocketManager { emitZoneMessage(subMessage, client); } - private toProtoZone(zone: Zone|null): ProtoZone|undefined { + private toProtoZone(zone: Zone | null): ProtoZone | undefined { if (zone !== null) { const zoneMessage = new ProtoZone(); zoneMessage.setX(zone.x); @@ -405,7 +417,6 @@ export class SocketManager { } private joinWebRtcRoom(user: User, group: Group) { - for (const otherUser of group.getUsers()) { if (user === otherUser) { continue; @@ -416,8 +427,8 @@ export class SocketManager { webrtcStartMessage1.setUserid(otherUser.id); webrtcStartMessage1.setName(otherUser.name); webrtcStartMessage1.setInitiator(true); - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+otherUser.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + otherUser.id, TURN_STATIC_AUTH_SECRET); webrtcStartMessage1.setWebrtcusername(username); webrtcStartMessage1.setWebrtcpassword(password); } @@ -426,16 +437,16 @@ export class SocketManager { serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); //if (!user.socket.disconnecting) { - user.socket.write(serverToClientMessage1); - //console.log('Sending webrtcstart initiator to '+user.socket.userId) + user.socket.write(serverToClientMessage1); + //console.log('Sending webrtcstart initiator to '+user.socket.userId) //} const webrtcStartMessage2 = new WebRtcStartMessage(); webrtcStartMessage2.setUserid(user.id); webrtcStartMessage2.setName(user.name); webrtcStartMessage2.setInitiator(false); - if (TURN_STATIC_AUTH_SECRET !== '') { - const {username, password} = this.getTURNCredentials(''+user.id, TURN_STATIC_AUTH_SECRET); + if (TURN_STATIC_AUTH_SECRET !== "") { + const { username, password } = this.getTURNCredentials("" + user.id, TURN_STATIC_AUTH_SECRET); webrtcStartMessage2.setWebrtcusername(username); webrtcStartMessage2.setWebrtcpassword(password); } @@ -444,10 +455,9 @@ export class SocketManager { serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); //if (!otherUser.socket.disconnecting) { - otherUser.socket.write(serverToClientMessage2); - //console.log('Sending webrtcstart to '+otherUser.socket.userId) + otherUser.socket.write(serverToClientMessage2); + //console.log('Sending webrtcstart to '+otherUser.socket.userId) //} - } } @@ -456,17 +466,17 @@ export class SocketManager { * and the Coturn server. * The Coturn server should be initialized with parameters: `--use-auth-secret --static-auth-secret=MySecretKey` */ - private getTURNCredentials(name: string, secret: string): {username: string, password: string} { - const unixTimeStamp = Math.floor(Date.now()/1000) + 4*3600; // this credential would be valid for the next 4 hours - const username = [unixTimeStamp, name].join(':'); - const hmac = crypto.createHmac('sha1', secret); - hmac.setEncoding('base64'); + private getTURNCredentials(name: string, secret: string): { username: string; password: string } { + const unixTimeStamp = Math.floor(Date.now() / 1000) + 4 * 3600; // this credential would be valid for the next 4 hours + const username = [unixTimeStamp, name].join(":"); + const hmac = crypto.createHmac("sha1", secret); + hmac.setEncoding("base64"); hmac.write(username); hmac.end(); const password = hmac.read(); return { username: username, - password: password + password: password, }; } @@ -489,10 +499,9 @@ export class SocketManager { serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1); //if (!otherUser.socket.disconnecting) { - otherUser.socket.write(serverToClientMessage1); + otherUser.socket.write(serverToClientMessage1); //} - const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage(); webrtcDisconnectMessage2.setUserid(otherUser.id); @@ -500,7 +509,7 @@ export class SocketManager { serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2); //if (!user.socket.disconnecting) { - user.socket.write(serverToClientMessage2); + user.socket.write(serverToClientMessage2); //} } } @@ -517,40 +526,41 @@ export class SocketManager { console.error('An error occurred on "emitPlayGlobalMessage" event'); console.error(e); } - } public getWorlds(): Map { return this.rooms; } - public handleQueryJitsiJwtMessage(user: User, queryJitsiJwtMessage: QueryJitsiJwtMessage) { const room = queryJitsiJwtMessage.getJitsiroom(); const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead. - if (SECRET_JITSI_KEY === '') { - throw new Error('You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.'); + if (SECRET_JITSI_KEY === "") { + throw new Error("You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi."); } // Let's see if the current client has const isAdmin = user.tags.includes(tag); - const jwt = Jwt.sign({ - "aud": "jitsi", - "iss": JITSI_ISS, - "sub": JITSI_URL, - "room": room, - "moderator": isAdmin - }, SECRET_JITSI_KEY, { - expiresIn: '1d', - algorithm: "HS256", - header: - { - "alg": "HS256", - "typ": "JWT" - } - }); + const jwt = Jwt.sign( + { + aud: "jitsi", + iss: JITSI_ISS, + sub: JITSI_URL, + room: room, + moderator: isAdmin, + }, + SECRET_JITSI_KEY, + { + expiresIn: "1d", + algorithm: "HS256", + header: { + alg: "HS256", + typ: "JWT", + }, + } + ); const sendJitsiJwtMessage = new SendJitsiJwtMessage(); sendJitsiJwtMessage.setJitsiroom(room); @@ -562,7 +572,7 @@ export class SocketManager { user.socket.write(serverToClientMessage); } - public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage){ + public handlerSendUserMessage(user: User, sendUserMessageToSend: SendUserMessage) { const sendUserMessage = new SendUserMessage(); sendUserMessage.setMessage(sendUserMessageToSend.getMessage()); sendUserMessage.setType(sendUserMessageToSend.getType()); @@ -572,7 +582,7 @@ export class SocketManager { user.socket.write(serverToClientMessage); } - public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage){ + public handlerBanUserMessage(room: GameRoom, user: User, banUserMessageToSend: BanUserMessage) { const banUserMessage = new BanUserMessage(); banUserMessage.setMessage(banUserMessageToSend.getMessage()); banUserMessage.setType(banUserMessageToSend.getType()); @@ -592,7 +602,7 @@ export class SocketManager { public addZoneListener(call: ZoneSocket, roomId: string, x: number, y: number): void { const room = this.rooms.get(roomId); if (!room) { - console.error("In addZoneListener, could not find room with id '" + roomId + "'"); + console.error("In addZoneListener, could not find room with id '" + roomId + "'"); return; } @@ -636,7 +646,7 @@ export class SocketManager { removeZoneListener(call: ZoneSocket, roomId: string, x: number, y: number) { const room = this.rooms.get(roomId); if (!room) { - console.error("In removeZoneListener, could not find room with id '" + roomId + "'"); + console.error("In removeZoneListener, could not find room with id '" + roomId + "'"); return; } @@ -651,7 +661,7 @@ export class SocketManager { return room; } - public leaveAdminRoom(room: GameRoom, admin: Admin){ + public leaveAdminRoom(room: GameRoom, admin: Admin) { room.adminLeave(admin); if (room.isEmpty()) { this.rooms.delete(room.roomId); @@ -663,19 +673,27 @@ export class SocketManager { public sendAdminMessage(roomId: string, recipientUuid: string, message: string): void { const room = this.rooms.get(roomId); if (!room) { - console.error("In sendAdminMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); + console.error( + "In sendAdminMessage, could not find room with id '" + + roomId + + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" + ); return; } const recipient = room.getUserByUuid(recipientUuid); if (recipient === undefined) { - console.error("In sendAdminMessage, could not find user with id '" + recipientUuid + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?"); + console.error( + "In sendAdminMessage, could not find user with id '" + + recipientUuid + + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?" + ); return; } const sendUserMessage = new SendUserMessage(); sendUserMessage.setMessage(message); - sendUserMessage.setType('ban'); //todo: is the type correct? + sendUserMessage.setType("ban"); //todo: is the type correct? const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setSendusermessage(sendUserMessage); @@ -686,13 +704,21 @@ export class SocketManager { public banUser(roomId: string, recipientUuid: string, message: string): void { const room = this.rooms.get(roomId); if (!room) { - console.error("In banUser, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); + console.error( + "In banUser, could not find room with id '" + + roomId + + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" + ); return; } const recipient = room.getUserByUuid(recipientUuid); if (recipient === undefined) { - console.error("In banUser, could not find user with id '" + recipientUuid + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?"); + console.error( + "In banUser, could not find user with id '" + + recipientUuid + + "'. Maybe the user left the room a few milliseconds ago and there was a race condition?" + ); return; } @@ -701,7 +727,7 @@ export class SocketManager { const banUserMessage = new BanUserMessage(); banUserMessage.setMessage(message); - banUserMessage.setType('banned'); + banUserMessage.setType("banned"); const serverToClientMessage = new ServerToClientMessage(); serverToClientMessage.setBanusermessage(banUserMessage); @@ -711,19 +737,22 @@ export class SocketManager { recipient.socket.end(); } - sendAdminRoomMessage(roomId: string, message: string) { const room = this.rooms.get(roomId); if (!room) { //todo: this should cause the http call to return a 500 - console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); + console.error( + "In sendAdminRoomMessage, could not find room with id '" + + roomId + + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" + ); return; } room.getUsers().forEach((recipient) => { const sendUserMessage = new SendUserMessage(); sendUserMessage.setMessage(message); - sendUserMessage.setType('message'); + sendUserMessage.setType("message"); const clientMessage = new ServerToClientMessage(); clientMessage.setSendusermessage(sendUserMessage); @@ -732,14 +761,18 @@ export class SocketManager { }); } - dispatchWorlFullWarning(roomId: string,): void { + dispatchWorlFullWarning(roomId: string): void { const room = this.rooms.get(roomId); if (!room) { //todo: this should cause the http call to return a 500 - console.error("In sendAdminRoomMessage, could not find room with id '" + roomId + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?"); + console.error( + "In sendAdminRoomMessage, could not find room with id '" + + roomId + + "'. Maybe the room was closed a few milliseconds ago and there was a race condition?" + ); return; } - + room.getUsers().forEach((recipient) => { const worldFullMessage = new WorldFullWarningMessage(); @@ -750,17 +783,17 @@ export class SocketManager { }); } - dispatchRoomRefresh(roomId: string,): void { + dispatchRoomRefresh(roomId: string): void { const room = this.rooms.get(roomId); if (!room) { return; } - + const versionNumber = room.incrementVersion(); room.getUsers().forEach((recipient) => { const worldFullMessage = new RefreshRoomMessage(); - worldFullMessage.setRoomid(roomId) - worldFullMessage.setVersionnumber(versionNumber) + worldFullMessage.setRoomid(roomId); + worldFullMessage.setVersionnumber(versionNumber); const clientMessage = new ServerToClientMessage(); clientMessage.setRefreshroommessage(worldFullMessage); diff --git a/front/dist/resources/objects/layout_modes.png b/front/dist/resources/objects/layout_modes.png deleted file mode 100644 index abd9adaf..00000000 Binary files a/front/dist/resources/objects/layout_modes.png and /dev/null differ diff --git a/front/package.json b/front/package.json index 1990a28a..3414e261 100644 --- a/front/package.json +++ b/front/package.json @@ -67,8 +67,8 @@ "lint": "node_modules/.bin/eslint src/ . --ext .ts", "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts", "precommit": "lint-staged", - "svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\" --watch", - "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore\"", + "svelte-check-watch": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\" --watch", + "svelte-check": "svelte-check --fail-on-warnings --fail-on-hints --compiler-warnings \"a11y-no-onchange:ignore,a11y-autofocus:ignore,a11y-media-has-caption:ignore\"", "pretty": "yarn prettier --write 'src/**/*.{ts,tsx}'", "pretty-check": "yarn prettier --check 'src/**/*.{ts,tsx}'" }, diff --git a/front/src/Components/App.svelte b/front/src/Components/App.svelte index 2e159d2d..8ade9398 100644 --- a/front/src/Components/App.svelte +++ b/front/src/Components/App.svelte @@ -1,5 +1,5 @@
@@ -68,6 +71,7 @@ --> {#if $gameOverlayVisibilityStore}
+
diff --git a/front/src/Components/CameraControls.svelte b/front/src/Components/CameraControls.svelte index 7bfa1c9c..e0027acd 100644 --- a/front/src/Components/CameraControls.svelte +++ b/front/src/Components/CameraControls.svelte @@ -7,6 +7,11 @@ import cinemaCloseImg from "./images/cinema-close.svg"; import microphoneImg from "./images/microphone.svg"; import microphoneCloseImg from "./images/microphone-close.svg"; + import layoutPresentationImg from "./images/layout-presentation.svg"; + import layoutChatImg from "./images/layout-chat.svg"; + import {layoutModeStore} from "../Stores/StreamableCollectionStore"; + import {LayoutMode} from "../WebRtc/LayoutManager"; + import {peerStore} from "../Stores/PeerStore"; function screenSharingClick(): void { if ($requestedScreenSharingState === true) { @@ -32,10 +37,24 @@ } } + function switchLayoutMode() { + if ($layoutModeStore === LayoutMode.Presentation) { + $layoutModeStore = LayoutMode.VideoChat; + } else { + $layoutModeStore = LayoutMode.Presentation; + } + }
-
+
+
+ {#if $layoutModeStore === LayoutMode.Presentation } + Switch to mosaic mode + {:else} + Switch to presentation mode + {/if} +
{#if $requestedMicrophoneState} Turn on microphone diff --git a/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte b/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte index a22da2fa..79ad1810 100644 --- a/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte +++ b/front/src/Components/EnableCamera/HorizontalSoundMeterWidget.svelte @@ -58,7 +58,7 @@
- {#each [...Array(NB_BARS).keys()] as i} + {#each [...Array(NB_BARS).keys()] as i (i)}
{/each}
diff --git a/front/src/Components/SoundMeterWidget.svelte b/front/src/Components/SoundMeterWidget.svelte index 30650e3f..74d3f7b3 100644 --- a/front/src/Components/SoundMeterWidget.svelte +++ b/front/src/Components/SoundMeterWidget.svelte @@ -6,8 +6,6 @@ export let stream: MediaStream|null; let volume = 0; - const NB_BARS = 5; - let timeout: ReturnType; const soundMeter = new SoundMeter(); let display = false; @@ -23,7 +21,7 @@ timeout = setInterval(() => { try{ - volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0)); + volume = soundMeter.getVolume(); //console.log(volume); }catch(err){ @@ -45,9 +43,9 @@
- 1}> - 2}> - 3}> - 4}> 5}> + 10}> + 15}> + 40}> + 70}>
diff --git a/front/src/Components/Video/ChatLayout.svelte b/front/src/Components/Video/ChatLayout.svelte new file mode 100644 index 00000000..ac91217e --- /dev/null +++ b/front/src/Components/Video/ChatLayout.svelte @@ -0,0 +1,35 @@ + + +
+ {#each [...$streamableCollectionStore.values()] as peer (peer.uniqueId)} + + {/each} +
diff --git a/front/src/Components/Video/LocalStreamMediaBox.svelte b/front/src/Components/Video/LocalStreamMediaBox.svelte new file mode 100644 index 00000000..55c7f22c --- /dev/null +++ b/front/src/Components/Video/LocalStreamMediaBox.svelte @@ -0,0 +1,16 @@ + + + +
+ {#if stream} + + {/if} +
diff --git a/front/src/Components/Video/MediaBox.svelte b/front/src/Components/Video/MediaBox.svelte new file mode 100644 index 00000000..9160a7a9 --- /dev/null +++ b/front/src/Components/Video/MediaBox.svelte @@ -0,0 +1,20 @@ + + +
+ {#if streamable instanceof VideoPeer} + + {:else if streamable instanceof ScreenSharingPeer} + + {:else} + + {/if} +
diff --git a/front/src/Components/Video/PresentationLayout.svelte b/front/src/Components/Video/PresentationLayout.svelte new file mode 100644 index 00000000..f68dd2f1 --- /dev/null +++ b/front/src/Components/Video/PresentationLayout.svelte @@ -0,0 +1,24 @@ + + +
+ {#if $videoFocusStore } + + {/if} +
+ diff --git a/front/src/Components/Video/ScreenSharingMediaBox.svelte b/front/src/Components/Video/ScreenSharingMediaBox.svelte new file mode 100644 index 00000000..c6e1564f --- /dev/null +++ b/front/src/Components/Video/ScreenSharingMediaBox.svelte @@ -0,0 +1,33 @@ + + +
+ {#if $statusStore === 'connecting'} +
+ {/if} + {#if $statusStore === 'error'} +
+ {/if} + {#if $streamStore === null} + {name} + {:else} + + {/if} +
+ + diff --git a/front/src/Components/Video/VideoMediaBox.svelte b/front/src/Components/Video/VideoMediaBox.svelte new file mode 100644 index 00000000..1a581914 --- /dev/null +++ b/front/src/Components/Video/VideoMediaBox.svelte @@ -0,0 +1,48 @@ + + +
+ {#if $statusStore === 'connecting'} +
+ {/if} + {#if $statusStore === 'error'} +
+ {/if} + {#if !$constraintStore || $constraintStore.video === false} + {name} + {/if} + {#if $constraintStore && $constraintStore.audio === false} + Muted + {/if} + + {#if $streamStore } + + {/if} + + {#if $constraintStore && $constraintStore.audio !== false} + + {/if} +
+ diff --git a/front/src/Components/Video/VideoOverlay.svelte b/front/src/Components/Video/VideoOverlay.svelte new file mode 100644 index 00000000..feb13743 --- /dev/null +++ b/front/src/Components/Video/VideoOverlay.svelte @@ -0,0 +1,23 @@ + + +
+ {#if $layoutModeStore === LayoutMode.Presentation } + + {:else } + + {/if} +
+ + diff --git a/front/src/Components/Video/images/blockSign.svg b/front/src/Components/Video/images/blockSign.svg new file mode 100644 index 00000000..c64ba294 --- /dev/null +++ b/front/src/Components/Video/images/blockSign.svg @@ -0,0 +1,22 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/front/src/Components/Video/images/report.svg b/front/src/Components/Video/images/report.svg new file mode 100644 index 00000000..14753256 --- /dev/null +++ b/front/src/Components/Video/images/report.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/Components/Video/utils.ts b/front/src/Components/Video/utils.ts new file mode 100644 index 00000000..ab7a63be --- /dev/null +++ b/front/src/Components/Video/utils.ts @@ -0,0 +1,27 @@ +export function getColorByString(str: string) : string|null { + let hash = 0; + if (str.length === 0) { + return null; + } + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + hash = hash & hash; + } + let color = '#'; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 255; + color += ('00' + value.toString(16)).substr(-2); + } + return color; +} + +export function srcObject(node: HTMLVideoElement, stream: MediaStream) { + node.srcObject = stream; + return { + update(newStream: MediaStream) { + if (node.srcObject != newStream) { + node.srcObject = newStream + } + } + } +} diff --git a/front/src/Components/images/layout-chat.svg b/front/src/Components/images/layout-chat.svg new file mode 100644 index 00000000..af071d49 --- /dev/null +++ b/front/src/Components/images/layout-chat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/Components/images/layout-presentation.svg b/front/src/Components/images/layout-presentation.svg new file mode 100644 index 00000000..bf65d002 --- /dev/null +++ b/front/src/Components/images/layout-presentation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 85d9ea4a..69005bad 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -27,7 +27,6 @@ import { import { TextureError } from "../../Exception/TextureError"; import type { UserMovedMessage } from "../../Messages/generated/messages_pb"; import { ProtobufClientUtils } from "../../Network/ProtobufClientUtils"; -import { peerStore } from "../../Stores/PeerStore"; import { touchScreenManager } from "../../Touch/TouchScreenManager"; import { urlManager } from "../../Url/UrlManager"; import { audioManager } from "../../WebRtc/AudioManager"; @@ -35,10 +34,10 @@ import { coWebsiteManager } from "../../WebRtc/CoWebsiteManager"; import { HtmlUtils } from "../../WebRtc/HtmlUtils"; import { jitsiFactory } from "../../WebRtc/JitsiFactory"; import { - AUDIO_LOOP_PROPERTY, AUDIO_VOLUME_PROPERTY, CenterListener, + AUDIO_LOOP_PROPERTY, AUDIO_VOLUME_PROPERTY, + Box, JITSI_MESSAGE_PROPERTIES, layoutManager, - LayoutMode, ON_ACTION_TRIGGER_BUTTON, TRIGGER_JITSI_PROPERTIES, TRIGGER_WEBSITE_PROPERTIES, @@ -94,6 +93,9 @@ import type { HasPlayerMovedEvent } from '../../Api/Events/HasPlayerMovedEvent'; import AnimatedTiles from "phaser-animated-tiles"; import {soundManager} from "./SoundManager"; +import {peerStore, screenSharingPeerStore} from "../../Stores/PeerStore"; +import {videoFocusStore} from "../../Stores/VideoFocusStore"; +import {biggestAvailableAreaStore} from "../../Stores/BiggestAvailableAreaStore"; export interface GameSceneInitInterface { initPosition: PointInterface | null, @@ -132,7 +134,7 @@ interface DeleteGroupEventInterface { const defaultStartLayerName = 'start'; -export class GameScene extends DirtyScene implements CenterListener { +export class GameScene extends DirtyScene { Terrains: Array; CurrentPlayer!: Player; MapPlayers!: Phaser.Physics.Arcade.Group; @@ -172,8 +174,6 @@ export class GameScene extends DirtyScene implements CenterListener { y: -1000 } - private presentationModeSprite!: Sprite; - private chatModeSprite!: Sprite; private gameMap!: GameMap; private actionableItems: Map = new Map(); // The item that can be selected by pressing the space key. @@ -277,7 +277,6 @@ export class GameScene extends DirtyScene implements CenterListener { this.onMapLoad(data); } - this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', { frameWidth: 32, frameHeight: 32 }); this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); //eslint-disable-next-line @typescript-eslint/no-explicit-any (this.load as any).rexWebFont({ @@ -497,10 +496,6 @@ export class GameScene extends DirtyScene implements CenterListener { this.outlinedItem?.activate(); }); - this.presentationModeSprite = new PresentationModeIcon(this, 36, this.game.renderer.height - 2); - this.presentationModeSprite.on('pointerup', this.switchLayoutMode.bind(this)); - this.chatModeSprite = new ChatModeIcon(this, 70, this.game.renderer.height - 2); - this.chatModeSprite.on('pointerup', this.switchLayoutMode.bind(this)); this.openChatIcon = new OpenChatIcon(this, 2, this.game.renderer.height - 2) // FIXME: change this to use the UserInputManager class for input @@ -512,7 +507,8 @@ export class GameScene extends DirtyScene implements CenterListener { this.reposition(); // From now, this game scene will be notified of reposition events - layoutManager.setListener(this); + biggestAvailableAreaStore.subscribe((box) => this.updateCameraOffset(box)); + this.triggerOnMapLayerPropertyChange(); this.listenToIframeEvents(); @@ -643,21 +639,19 @@ export class GameScene extends DirtyScene implements CenterListener { // When connection is performed, let's connect SimplePeer this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName); peerStore.connectToSimplePeer(this.simplePeer); + screenSharingPeerStore.connectToSimplePeer(this.simplePeer); + videoFocusStore.connectToSimplePeer(this.simplePeer); this.GlobalMessageManager = new GlobalMessageManager(this.connection); userMessageManager.setReceiveBanListener(this.bannedUser.bind(this)); const self = this; this.simplePeer.registerPeerConnectionListener({ - onConnect(user: UserSimplePeerInterface) { - self.presentationModeSprite.setVisible(true); - self.chatModeSprite.setVisible(true); + onConnect(peer) { self.openChatIcon.setVisible(true); audioManager.decreaseVolume(); }, onDisconnect(userId: number) { if (self.simplePeer.getNbConnections() === 0) { - self.presentationModeSprite.setVisible(false); - self.chatModeSprite.setVisible(false); self.openChatIcon.setVisible(false); audioManager.restoreVolume(); } @@ -1077,23 +1071,6 @@ ${escapedMessage} this.MapPlayersByKey = new Map(); } - private switchLayoutMode(): void { - //if discussion is activated, this layout cannot be activated - if (mediaManager.activatedDiscussion) { - return; - } - const mode = layoutManager.getLayoutMode(); - if (mode === LayoutMode.Presentation) { - layoutManager.switchLayoutMode(LayoutMode.VideoChat); - this.presentationModeSprite.setFrame(1); - this.chatModeSprite.setFrame(2); - } else { - layoutManager.switchLayoutMode(LayoutMode.Presentation); - this.presentationModeSprite.setFrame(0); - this.chatModeSprite.setFrame(3); - } - } - private initStartXAndStartY() { // If there is an init position passed if (this.initPosition !== null) { @@ -1206,7 +1183,7 @@ ${escapedMessage} initCamera() { this.cameras.main.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels); this.cameras.main.startFollow(this.CurrentPlayer, true); - this.updateCameraOffset(); + biggestAvailableAreaStore.recompute(); } createCollisionWithPlayer() { @@ -1353,7 +1330,7 @@ ${escapedMessage} * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. */ update(time: number, delta: number): void { - mediaManager.updateScene(); + this.dirty = false; this.currentTick = time; this.CurrentPlayer.moveUser(delta); @@ -1583,20 +1560,17 @@ ${escapedMessage} } private reposition(): void { - this.presentationModeSprite.setY(this.game.renderer.height - 2); - this.chatModeSprite.setY(this.game.renderer.height - 2); this.openChatIcon.setY(this.game.renderer.height - 2); // Recompute camera offset if needed - this.updateCameraOffset(); + biggestAvailableAreaStore.recompute(); } /** * Updates the offset of the character compared to the center of the screen according to the layout manager * (tries to put the character in the center of the remaining space if there is a discussion going on. */ - private updateCameraOffset(): void { - const array = layoutManager.findBiggestAvailableArray(); + private updateCameraOffset(array: Box): void { const xCenter = (array.xEnd - array.xStart) / 2 + array.xStart; const yCenter = (array.yEnd - array.yStart) / 2 + array.yStart; @@ -1606,10 +1580,6 @@ ${escapedMessage} this.cameras.main.setFollowOffset((xCenter - game.offsetWidth / 2) * window.devicePixelRatio / this.scale.zoom, (yCenter - game.offsetHeight / 2) * window.devicePixelRatio / this.scale.zoom); } - public onCenterChange(): void { - this.updateCameraOffset(); - } - public startJitsi(roomName: string, jwt?: string): void { const allProps = this.gameMap.getCurrentProperties(); const jitsiConfig = this.safeParseJSONstring(allProps.get("jitsiConfig") as string | undefined, 'jitsiConfig'); @@ -1696,6 +1666,6 @@ ${escapedMessage} zoomByFactor(zoomFactor: number) { waScaleManager.zoomModifier *= zoomFactor; - this.updateCameraOffset(); + biggestAvailableAreaStore.recompute(); } } diff --git a/front/src/Phaser/Menu/MenuScene.ts b/front/src/Phaser/Menu/MenuScene.ts index bf9c9a08..4fc58f5d 100644 --- a/front/src/Phaser/Menu/MenuScene.ts +++ b/front/src/Phaser/Menu/MenuScene.ts @@ -3,17 +3,17 @@ import {SelectCharacterScene, SelectCharacterSceneName} from "../Login/SelectCha import {SelectCompanionScene, SelectCompanionSceneName} from "../Login/SelectCompanionScene"; import {gameManager} from "../Game/GameManager"; import {localUserStore} from "../../Connexion/LocalUserStore"; -import {mediaManager} from "../../WebRtc/MediaManager"; import {gameReportKey, gameReportRessource, ReportMenu} from "./ReportMenu"; import {connectionManager} from "../../Connexion/ConnectionManager"; import {GameConnexionTypes} from "../../Url/UrlManager"; import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer"; import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream"; import {menuIconVisible} from "../../Stores/MenuStore"; +import {videoConstraintStore} from "../../Stores/MediaStore"; +import {showReportScreenStore} from "../../Stores/ShowReportScreenStore"; import { HtmlUtils } from '../../WebRtc/HtmlUtils'; import { iframeListener } from '../../Api/IframeListener'; import { Subscription } from 'rxjs'; -import { videoConstraintStore } from "../../Stores/MediaStore"; import {registerMenuCommandStream} from "../../Api/Events/ui/MenuItemRegisterEvent"; import {sendMenuClickedEvent} from "../../Api/iframe/Ui/MenuItem"; import {consoleGlobalMessageManagerVisibleStore} from "../../Stores/ConsoleGlobalMessageManagerStore"; @@ -111,9 +111,11 @@ export class MenuScene extends Phaser.Scene { }); this.gameReportElement = new ReportMenu(this, connectionManager.getConnexionType === GameConnexionTypes.anonymous); - mediaManager.setShowReportModalCallBacks((userId, userName) => { + showReportScreenStore.subscribe((user) => { this.closeAll(); - this.gameReportElement.open(parseInt(userId), userName); + if (user !== null) { + this.gameReportElement.open(user.userId, user.userName); + } }); this.input.keyboard.on('keyup-TAB', () => { diff --git a/front/src/Stores/BiggestAvailableAreaStore.ts b/front/src/Stores/BiggestAvailableAreaStore.ts new file mode 100644 index 00000000..716f37fc --- /dev/null +++ b/front/src/Stores/BiggestAvailableAreaStore.ts @@ -0,0 +1,127 @@ +import {get, writable} from "svelte/store"; +import type {Box} from "../WebRtc/LayoutManager"; +import {HtmlUtils} from "../WebRtc/HtmlUtils"; +import {LayoutMode} from "../WebRtc/LayoutManager"; +import {layoutModeStore} from "./StreamableCollectionStore"; + +/** + * Tries to find the biggest available box of remaining space (this is a space where we can center the character) + */ +function findBiggestAvailableArea(): Box { + const game = HtmlUtils.querySelectorOrFail('#game canvas'); + if (get(layoutModeStore) === LayoutMode.VideoChat) { + const children = document.querySelectorAll('div.chat-mode > div'); + const htmlChildren = Array.from(children.values()); + + // No chat? Let's go full center + if (htmlChildren.length === 0) { + return { + xStart: 0, + yStart: 0, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } + + const lastDiv = htmlChildren[htmlChildren.length - 1]; + // Compute area between top right of the last div and bottom right of window + const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth)) + * (game.offsetHeight - lastDiv.offsetTop); + + // Compute area between bottom of last div and bottom of the screen on whole width + const area2 = game.offsetWidth + * (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight)); + + if (area1 < 0 && area2 < 0) { + // If screen is full, let's not attempt something foolish and simply center character in the middle. + return { + xStart: 0, + yStart: 0, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } + if (area1 <= area2) { + return { + xStart: 0, + yStart: lastDiv.offsetTop + lastDiv.offsetHeight, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } else { + return { + xStart: lastDiv.offsetLeft + lastDiv.offsetWidth, + yStart: lastDiv.offsetTop, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } + } else { + // Possible destinations: at the center bottom or at the right bottom. + const mainSectionChildren = Array.from(document.querySelectorAll('div.main-section > div').values()); + const sidebarChildren = Array.from(document.querySelectorAll('aside.sidebar > div').values()); + + // No presentation? Let's center on the screen + if (mainSectionChildren.length === 0) { + return { + xStart: 0, + yStart: 0, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } + + // At this point, we know we have at least one element in the main section. + const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1]; + + const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight)) + * (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth); + + let leftSideBar: number; + let bottomSideBar: number; + if (sidebarChildren.length === 0) { + leftSideBar = HtmlUtils.getElementByIdOrFail('sidebar').offsetLeft; + bottomSideBar = 0; + } else { + const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1]; + leftSideBar = lastSideBarChildren.offsetLeft; + bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight; + } + const sideBarArea = (game.offsetWidth - leftSideBar) + * (game.offsetHeight - bottomSideBar); + + if (presentationArea <= sideBarArea) { + return { + xStart: leftSideBar, + yStart: bottomSideBar, + xEnd: game.offsetWidth, + yEnd: game.offsetHeight + } + } else { + return { + xStart: 0, + yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight, + xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area + yEnd: game.offsetHeight + } + } + } +} + + +/** + * A store that contains the list of (video) peers we are connected to. + */ +function createBiggestAvailableAreaStore() { + + const { subscribe, set } = writable({xStart:0, yStart: 0, xEnd: 1, yEnd: 1}); + + return { + subscribe, + recompute: () => { + set(findBiggestAvailableArea()); + } + }; +} + +export const biggestAvailableAreaStore = createBiggestAvailableAreaStore(); diff --git a/front/src/Stores/GameOverlayStoreVisibility.ts b/front/src/Stores/GameOverlayStoreVisibility.ts new file mode 100644 index 00000000..c58c929d --- /dev/null +++ b/front/src/Stores/GameOverlayStoreVisibility.ts @@ -0,0 +1,17 @@ +import {writable} from "svelte/store"; + +/** + * A store that contains whether the game overlay is shown or not. + * Typically, the overlay is hidden when entering Jitsi meet. + */ +function createGameOverlayVisibilityStore() { + const { subscribe, set, update } = writable(false); + + return { + subscribe, + showGameOverlay: () => set(true), + hideGameOverlay: () => set(false), + }; +} + +export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore(); diff --git a/front/src/Stores/MediaStore.ts b/front/src/Stores/MediaStore.ts index d622511e..6cb9f75c 100644 --- a/front/src/Stores/MediaStore.ts +++ b/front/src/Stores/MediaStore.ts @@ -1,13 +1,14 @@ import {derived, get, Readable, readable, writable, Writable} from "svelte/store"; -import {peerStore} from "./PeerStore"; import {localUserStore} from "../Connexion/LocalUserStore"; -import {ITiledMapGroupLayer, ITiledMapObjectLayer, ITiledMapTileLayer} from "../Phaser/Map/ITiledMap"; import {userMovingStore} from "./GameStore"; import {HtmlUtils} from "../WebRtc/HtmlUtils"; import {BrowserTooOldError} from "./Errors/BrowserTooOldError"; import {errorStore} from "./ErrorStore"; import {isIOS} from "../WebRtc/DeviceUtils"; import {WebviewOnOldIOS} from "./Errors/WebviewOnOldIOS"; +import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility"; +import {peerStore} from "./PeerStore"; +import {privacyShutdownStore} from "./PrivacyShutdownStore"; /** * A store that contains the camera state requested by the user (on or off). @@ -35,35 +36,6 @@ function createRequestedMicrophoneState() { }; } -/** - * A store containing whether the current page is visible or not. - */ -export const visibilityStore = readable(document.visibilityState === 'visible', function start(set) { - const onVisibilityChange = () => { - set(document.visibilityState === 'visible'); - }; - - document.addEventListener('visibilitychange', onVisibilityChange); - - return function stop() { - document.removeEventListener('visibilitychange', onVisibilityChange); - }; -}); - -/** - * A store that contains whether the game overlay is shown or not. - * Typically, the overlay is hidden when entering Jitsi meet. - */ -function createGameOverlayVisibilityStore() { - const { subscribe, set, update } = writable(false); - - return { - subscribe, - showGameOverlay: () => set(true), - hideGameOverlay: () => set(false), - }; -} - /** * A store that contains whether the EnableCameraScene is shown or not. */ @@ -79,44 +51,8 @@ function createEnableCameraSceneVisibilityStore() { export const requestedCameraState = createRequestedCameraState(); export const requestedMicrophoneState = createRequestedMicrophoneState(); -export const gameOverlayVisibilityStore = createGameOverlayVisibilityStore(); export const enableCameraSceneVisibilityStore = createEnableCameraSceneVisibilityStore(); -/** - * A store that contains "true" if the webcam should be stopped for privacy reasons - i.e. if the the user left the the page while not in a discussion. - */ -function createPrivacyShutdownStore() { - let privacyEnabled = false; - - const { subscribe, set, update } = writable(privacyEnabled); - - visibilityStore.subscribe((isVisible) => { - if (!isVisible && get(peerStore).size === 0) { - privacyEnabled = true; - set(true); - } - if (isVisible) { - privacyEnabled = false; - set(false); - } - }); - - peerStore.subscribe((peers) => { - if (peers.size === 0 && get(visibilityStore) === false) { - privacyEnabled = true; - set(true); - } - }); - - - return { - subscribe, - }; -} - -export const privacyShutdownStore = createPrivacyShutdownStore(); - - /** * A store containing whether the webcam was enabled in the last 10 seconds */ diff --git a/front/src/Stores/PeerStore.ts b/front/src/Stores/PeerStore.ts index a582e692..725c8940 100644 --- a/front/src/Stores/PeerStore.ts +++ b/front/src/Stores/PeerStore.ts @@ -1,26 +1,62 @@ -import { derived, writable, Writable } from "svelte/store"; -import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer"; -import type {SimplePeer} from "../WebRtc/SimplePeer"; +import {readable, writable} from "svelte/store"; +import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer"; +import {VideoPeer} from "../WebRtc/VideoPeer"; +import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer"; /** - * A store that contains the camera state requested by the user (on or off). + * A store that contains the list of (video) peers we are connected to. */ function createPeerStore() { - let users = new Map(); + let peers = new Map(); - const { subscribe, set, update } = writable(users); + const { subscribe, set, update } = writable(peers); return { subscribe, connectToSimplePeer: (simplePeer: SimplePeer) => { - users = new Map(); - set(users); + peers = new Map(); + set(peers); simplePeer.registerPeerConnectionListener({ - onConnect(user: UserSimplePeerInterface) { + onConnect(peer: RemotePeer) { + if (peer instanceof VideoPeer) { + update(users => { + users.set(peer.userId, peer); + return users; + }); + } + }, + onDisconnect(userId: number) { update(users => { - users.set(user.userId, user); + users.delete(userId); return users; }); + } + }) + } + }; +} + +/** + * A store that contains the list of screen sharing peers we are connected to. + */ +function createScreenSharingPeerStore() { + let peers = new Map(); + + const { subscribe, set, update } = writable(peers); + + return { + subscribe, + connectToSimplePeer: (simplePeer: SimplePeer) => { + peers = new Map(); + set(peers); + simplePeer.registerPeerConnectionListener({ + onConnect(peer: RemotePeer) { + if (peer instanceof ScreenSharingPeer) { + update(users => { + users.set(peer.userId, peer); + return users; + }); + } }, onDisconnect(userId: number) { update(users => { @@ -34,3 +70,56 @@ function createPeerStore() { } export const peerStore = createPeerStore(); +export const screenSharingPeerStore = createScreenSharingPeerStore(); + +/** + * A store that contains ScreenSharingPeer, ONLY if those ScreenSharingPeer are emitting a stream towards us! + */ +function createScreenSharingStreamStore() { + let peers = new Map(); + + return readable>(peers, function start(set) { + + let unsubscribes: (()=>void)[] = []; + + const unsubscribe = screenSharingPeerStore.subscribe((screenSharingPeers) => { + for (const unsubscribe of unsubscribes) { + unsubscribe(); + } + unsubscribes = []; + + peers = new Map(); + + screenSharingPeers.forEach((screenSharingPeer: ScreenSharingPeer, key: number) => { + + if (screenSharingPeer.isReceivingScreenSharingStream()) { + peers.set(key, screenSharingPeer); + } + + unsubscribes.push(screenSharingPeer.streamStore.subscribe((stream) => { + if (stream) { + peers.set(key, screenSharingPeer); + } else { + peers.delete(key); + } + set(peers); + })); + + }); + + set(peers); + + }); + + return function stop() { + unsubscribe(); + for (const unsubscribe of unsubscribes) { + unsubscribe(); + } + }; + }) +} + +export const screenSharingStreamStore = createScreenSharingStreamStore(); + + diff --git a/front/src/Stores/PrivacyShutdownStore.ts b/front/src/Stores/PrivacyShutdownStore.ts new file mode 100644 index 00000000..cd9cb1b9 --- /dev/null +++ b/front/src/Stores/PrivacyShutdownStore.ts @@ -0,0 +1,37 @@ +import {get, writable} from "svelte/store"; +import {peerStore} from "./PeerStore"; +import {visibilityStore} from "./VisibilityStore"; + +/** + * A store that contains "true" if the webcam should be stopped for privacy reasons - i.e. if the the user left the the page while not in a discussion. + */ +function createPrivacyShutdownStore() { + let privacyEnabled = false; + + const { subscribe, set, update } = writable(privacyEnabled); + + visibilityStore.subscribe((isVisible) => { + if (!isVisible && get(peerStore).size === 0) { + privacyEnabled = true; + set(true); + } + if (isVisible) { + privacyEnabled = false; + set(false); + } + }); + + peerStore.subscribe((peers) => { + if (peers.size === 0 && get(visibilityStore) === false) { + privacyEnabled = true; + set(true); + } + }); + + + return { + subscribe, + }; +} + +export const privacyShutdownStore = createPrivacyShutdownStore(); diff --git a/front/src/Stores/ScreenSharingStore.ts b/front/src/Stores/ScreenSharingStore.ts index ec5aa46f..ccb8c02c 100644 --- a/front/src/Stores/ScreenSharingStore.ts +++ b/front/src/Stores/ScreenSharingStore.ts @@ -1,16 +1,10 @@ import {derived, get, Readable, readable, writable, Writable} from "svelte/store"; import {peerStore} from "./PeerStore"; -import {localUserStore} from "../Connexion/LocalUserStore"; -import {ITiledMapGroupLayer, ITiledMapObjectLayer, ITiledMapTileLayer} from "../Phaser/Map/ITiledMap"; -import {userMovingStore} from "./GameStore"; -import {HtmlUtils} from "../WebRtc/HtmlUtils"; -import { - audioConstraintStore, cameraEnergySavingStore, - enableCameraSceneVisibilityStore, - gameOverlayVisibilityStore, LocalStreamStoreValue, privacyShutdownStore, - requestedCameraState, - requestedMicrophoneState, videoConstraintStore +import type { + LocalStreamStoreValue, } from "./MediaStore"; +import {DivImportance} from "../WebRtc/LayoutManager"; +import {gameOverlayVisibilityStore} from "./GameOverlayStoreVisibility"; declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any @@ -191,3 +185,33 @@ export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set) set($peerStore.size !== 0); }); + +export interface ScreenSharingLocalMedia { + uniqueId: string; + stream: MediaStream|null; + //subscribe(this: void, run: Subscriber, invalidate?: (value?: ScreenSharingLocalMedia) => void): Unsubscriber; +} + +/** + * The representation of the screen sharing stream. + */ +export const screenSharingLocalMedia = readable(null, function start(set) { + + const localMedia: ScreenSharingLocalMedia = { + uniqueId: "localScreenSharingStream", + stream: null + } + + const unsubscribe = screenSharingLocalStreamStore.subscribe((screenSharingLocalStream) => { + if (screenSharingLocalStream.type === "success") { + localMedia.stream = screenSharingLocalStream.stream; + } else { + localMedia.stream = null; + } + set(localMedia); + }); + + return function stop() { + unsubscribe(); + }; +}) diff --git a/front/src/Stores/ShowReportScreenStore.ts b/front/src/Stores/ShowReportScreenStore.ts new file mode 100644 index 00000000..f05e545a --- /dev/null +++ b/front/src/Stores/ShowReportScreenStore.ts @@ -0,0 +1,3 @@ +import {writable} from "svelte/store"; + +export const showReportScreenStore = writable<{userId: number, userName: string}|null>(null); diff --git a/front/src/Stores/StreamableCollectionStore.ts b/front/src/Stores/StreamableCollectionStore.ts new file mode 100644 index 00000000..bbcd354f --- /dev/null +++ b/front/src/Stores/StreamableCollectionStore.ts @@ -0,0 +1,43 @@ +import {derived, get, Readable, writable} from "svelte/store"; +import {ScreenSharingLocalMedia, screenSharingLocalMedia} from "./ScreenSharingStore"; +import { peerStore, screenSharingStreamStore} from "./PeerStore"; +import type {RemotePeer} from "../WebRtc/SimplePeer"; +import {LayoutMode} from "../WebRtc/LayoutManager"; + +export type Streamable = RemotePeer | ScreenSharingLocalMedia; + +export const layoutModeStore = writable(LayoutMode.Presentation); + +/** + * A store that contains everything that can produce a stream (so the peers + the local screen sharing stream) + */ +function createStreamableCollectionStore(): Readable> { + + return derived([ + screenSharingStreamStore, + peerStore, + screenSharingLocalMedia, + ], ([ + $screenSharingStreamStore, + $peerStore, + $screenSharingLocalMedia, + ], set) => { + + const peers = new Map(); + + const addPeer = (peer: Streamable) => { + peers.set(peer.uniqueId, peer); + }; + + $screenSharingStreamStore.forEach(addPeer); + $peerStore.forEach(addPeer); + + if ($screenSharingLocalMedia?.stream) { + addPeer($screenSharingLocalMedia); + } + + set(peers); + }); +} + +export const streamableCollectionStore = createStreamableCollectionStore(); diff --git a/front/src/Stores/VideoFocusStore.ts b/front/src/Stores/VideoFocusStore.ts new file mode 100644 index 00000000..28e948ce --- /dev/null +++ b/front/src/Stores/VideoFocusStore.ts @@ -0,0 +1,47 @@ +import {writable} from "svelte/store"; +import type {RemotePeer, SimplePeer} from "../WebRtc/SimplePeer"; +import {VideoPeer} from "../WebRtc/VideoPeer"; +import {ScreenSharingPeer} from "../WebRtc/ScreenSharingPeer"; +import type {Streamable} from "./StreamableCollectionStore"; + +/** + * A store that contains the peer / media that has currently the "importance" focus. + */ +function createVideoFocusStore() { + const { subscribe, set, update } = writable(null); + + let focusedMedia: Streamable | null = null; + + return { + subscribe, + focus: (media: Streamable) => { + focusedMedia = media; + set(media); + }, + removeFocus: () => { + focusedMedia = null; + set(null); + }, + toggleFocus: (media: Streamable) => { + if (media !== focusedMedia) { + focusedMedia = media; + } else { + focusedMedia = null; + } + set(focusedMedia); + }, + connectToSimplePeer: (simplePeer: SimplePeer) => { + simplePeer.registerPeerConnectionListener({ + onConnect(peer: RemotePeer) { + }, + onDisconnect(userId: number) { + if ((focusedMedia instanceof VideoPeer || focusedMedia instanceof ScreenSharingPeer) && focusedMedia.userId === userId) { + set(null); + } + } + }) + } + }; +} + +export const videoFocusStore = createVideoFocusStore(); diff --git a/front/src/Stores/VisibilityStore.ts b/front/src/Stores/VisibilityStore.ts new file mode 100644 index 00000000..4a13ffae --- /dev/null +++ b/front/src/Stores/VisibilityStore.ts @@ -0,0 +1,16 @@ +import {readable} from "svelte/store"; + +/** + * A store containing whether the current page is visible or not. + */ +export const visibilityStore = readable(document.visibilityState === 'visible', function start(set) { + const onVisibilityChange = () => { + set(document.visibilityState === 'visible'); + }; + + document.addEventListener('visibilitychange', onVisibilityChange); + + return function stop() { + document.removeEventListener('visibilitychange', onVisibilityChange); + }; +}); diff --git a/front/src/WebRtc/DiscussionManager.ts b/front/src/WebRtc/DiscussionManager.ts index 88b12b83..9d281e12 100644 --- a/front/src/WebRtc/DiscussionManager.ts +++ b/front/src/WebRtc/DiscussionManager.ts @@ -1,9 +1,9 @@ import {HtmlUtils} from "./HtmlUtils"; -import type {ShowReportCallBack} from "./MediaManager"; import type {UserInputManager} from "../Phaser/UserInput/UserInputManager"; import {connectionManager} from "../Connexion/ConnectionManager"; import {GameConnexionTypes} from "../Url/UrlManager"; import {iframeListener} from "../Api/IframeListener"; +import {showReportScreenStore} from "../Stores/ShowReportScreenStore"; export type SendMessageCallback = (message:string) => void; @@ -104,11 +104,10 @@ export class DiscussionManager { } public addParticipant( - userId: number|string, + userId: number|'me', name: string|undefined, img?: string|undefined, isMe: boolean = false, - showReportCallBack?: ShowReportCallBack ) { const divParticipant: HTMLDivElement = document.createElement('div'); divParticipant.classList.add('participant'); @@ -132,16 +131,13 @@ export class DiscussionManager { !isMe && connectionManager.getConnexionType && connectionManager.getConnexionType !== GameConnexionTypes.anonymous + && userId !== 'me' ) { const reportBanUserAction: HTMLButtonElement = document.createElement('button'); reportBanUserAction.classList.add('report-btn') reportBanUserAction.innerText = 'Report'; reportBanUserAction.addEventListener('click', () => { - if(showReportCallBack) { - showReportCallBack(`${userId}`, name); - }else{ - console.info('report feature is not activated!'); - } + showReportScreenStore.set({ userId: userId, userName: name ? name : ''}); }); divParticipant.appendChild(reportBanUserAction); } diff --git a/front/src/WebRtc/LayoutManager.ts b/front/src/WebRtc/LayoutManager.ts index 2d2bc844..1e25c0f2 100644 --- a/front/src/WebRtc/LayoutManager.ts +++ b/front/src/WebRtc/LayoutManager.ts @@ -16,14 +16,6 @@ export enum DivImportance { Normal = "Normal", } -/** - * Classes implementing this interface can be notified when the center of the screen (the player position) should be - * changed. - */ -export interface CenterListener { - onCenterChange(): void; -} - export const ON_ACTION_TRIGGER_BUTTON = 'onaction'; export const TRIGGER_WEBSITE_PROPERTIES = 'openWebsiteTrigger'; @@ -35,294 +27,12 @@ export const JITSI_MESSAGE_PROPERTIES = 'jitsiTriggerMessage'; export const AUDIO_VOLUME_PROPERTY = 'audioVolume'; export const AUDIO_LOOP_PROPERTY = 'audioLoop'; -/** - * This class is in charge of the video-conference layout. - * It receives positioning requests for videos and does its best to place them on the screen depending on the active layout mode. - */ +export type Box = {xStart: number, yStart: number, xEnd: number, yEnd: number}; + class LayoutManager { - private mode: LayoutMode = LayoutMode.Presentation; - - private importantDivs: Map = new Map(); - private normalDivs: Map = new Map(); - private listener: CenterListener|null = null; - private actionButtonTrigger: Map = new Map(); private actionButtonInformation: Map = new Map(); - public setListener(centerListener: CenterListener|null) { - this.listener = centerListener; - } - - public add(importance: DivImportance, userId: string, html: string): void { - const div = document.createElement('div'); - div.innerHTML = html; - div.id = "user-"+userId; - div.className = "media-container" - div.classList.add("nes-container", "is-rounded", "is-dark"); - div.onclick = () => { - const parentId = div.parentElement?.id; - if (parentId === 'sidebar' || parentId === 'chat-mode') { - this.focusOn(userId); - } else { - this.removeFocusOn(userId); - } - } - - if (importance === DivImportance.Important) { - this.importantDivs.set(userId, div); - - // If this is the first video with high importance, let's switch mode automatically. - if (this.importantDivs.size === 1 && this.mode === LayoutMode.VideoChat) { - this.switchLayoutMode(LayoutMode.Presentation); - } - } else if (importance === DivImportance.Normal) { - this.normalDivs.set(userId, div); - } else { - throw new Error('Unexpected importance'); - } - - this.positionDiv(div, importance); - this.adjustVideoChatClass(); - this.listener?.onCenterChange(); - } - - private positionDiv(elem: HTMLDivElement, importance: DivImportance): void { - if (this.mode === LayoutMode.VideoChat) { - const chatModeDiv = HtmlUtils.getElementByIdOrFail('chat-mode'); - chatModeDiv.appendChild(elem); - } else { - if (importance === DivImportance.Important) { - const mainSectionDiv = HtmlUtils.getElementByIdOrFail('main-section'); - mainSectionDiv.appendChild(elem); - } else if (importance === DivImportance.Normal) { - const sideBarDiv = HtmlUtils.getElementByIdOrFail('sidebar'); - sideBarDiv.appendChild(elem); - } - } - } - - /** - * Put the screen in presentation mode and move elem in presentation mode (and all other videos in normal mode) - */ - private focusOn(userId: string): void { - const focusedDiv = this.getDivByUserId(userId); - for (const [importantUserId, importantDiv] of this.importantDivs.entries()) { - //this.positionDiv(importantDiv, DivImportance.Normal); - this.importantDivs.delete(importantUserId); - this.normalDivs.set(importantUserId, importantDiv); - } - this.normalDivs.delete(userId); - this.importantDivs.set(userId, focusedDiv); - //this.positionDiv(focusedDiv, DivImportance.Important); - this.switchLayoutMode(LayoutMode.Presentation); - } - - /** - * Removes userId from presentation mode - */ - private removeFocusOn(userId: string): void { - const importantDiv = this.importantDivs.get(userId); - if (importantDiv === undefined) { - throw new Error('Div with user id "'+userId+'" is not in important mode'); - } - this.normalDivs.set(userId, importantDiv); - this.importantDivs.delete(userId); - - this.positionDiv(importantDiv, DivImportance.Normal); - } - - private getDivByUserId(userId: string): HTMLDivElement { - let div = this.importantDivs.get(userId); - if (div !== undefined) { - return div; - } - div = this.normalDivs.get(userId); - if (div !== undefined) { - return div; - } - throw new Error('Could not find media with user id '+userId); - } - - /** - * Removes the DIV matching userId. - */ - public remove(userId: string): void { - console.log('Removing video for userID '+userId+'.'); - let div = this.importantDivs.get(userId); - if (div !== undefined) { - div.remove(); - this.importantDivs.delete(userId); - this.adjustVideoChatClass(); - this.listener?.onCenterChange(); - return; - } - - div = this.normalDivs.get(userId); - if (div !== undefined) { - div.remove(); - this.normalDivs.delete(userId); - this.adjustVideoChatClass(); - this.listener?.onCenterChange(); - return; - } - - console.log('Cannot remove userID '+userId+'. Already removed?'); - //throw new Error('Could not find user ID "'+userId+'"'); - } - - private adjustVideoChatClass(): void { - const chatModeDiv = HtmlUtils.getElementByIdOrFail('chat-mode'); - chatModeDiv.classList.remove('one-col', 'two-col', 'three-col', 'four-col'); - - const nbUsers = this.importantDivs.size + this.normalDivs.size; - - if (nbUsers <= 1) { - chatModeDiv.classList.add('one-col'); - } else if (nbUsers <= 4) { - chatModeDiv.classList.add('two-col'); - } else if (nbUsers <= 9) { - chatModeDiv.classList.add('three-col'); - } else { - chatModeDiv.classList.add('four-col'); - } - } - - public switchLayoutMode(layoutMode: LayoutMode) { - this.mode = layoutMode; - - if (layoutMode === LayoutMode.Presentation) { - HtmlUtils.getElementByIdOrFail('sidebar').style.display = 'flex'; - HtmlUtils.getElementByIdOrFail('main-section').style.display = 'flex'; - HtmlUtils.getElementByIdOrFail('chat-mode').style.display = 'none'; - } else { - HtmlUtils.getElementByIdOrFail('sidebar').style.display = 'none'; - HtmlUtils.getElementByIdOrFail('main-section').style.display = 'none'; - HtmlUtils.getElementByIdOrFail('chat-mode').style.display = 'grid'; - } - - for (const div of this.importantDivs.values()) { - this.positionDiv(div, DivImportance.Important); - } - for (const div of this.normalDivs.values()) { - this.positionDiv(div, DivImportance.Normal); - } - this.listener?.onCenterChange(); - } - - public getLayoutMode(): LayoutMode { - return this.mode; - } - - /*public getGameCenter(): {x: number, y: number} { - - }*/ - - /** - * Tries to find the biggest available box of remaining space (this is a space where we can center the character) - */ - public findBiggestAvailableArray(): {xStart: number, yStart: number, xEnd: number, yEnd: number} { - const game = HtmlUtils.querySelectorOrFail('#game canvas'); - if (this.mode === LayoutMode.VideoChat) { - const children = document.querySelectorAll('div.chat-mode > div'); - const htmlChildren = Array.from(children.values()); - - // No chat? Let's go full center - if (htmlChildren.length === 0) { - return { - xStart: 0, - yStart: 0, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } - - const lastDiv = htmlChildren[htmlChildren.length - 1]; - // Compute area between top right of the last div and bottom right of window - const area1 = (game.offsetWidth - (lastDiv.offsetLeft + lastDiv.offsetWidth)) - * (game.offsetHeight - lastDiv.offsetTop); - - // Compute area between bottom of last div and bottom of the screen on whole width - const area2 = game.offsetWidth - * (game.offsetHeight - (lastDiv.offsetTop + lastDiv.offsetHeight)); - - if (area1 < 0 && area2 < 0) { - // If screen is full, let's not attempt something foolish and simply center character in the middle. - return { - xStart: 0, - yStart: 0, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } - if (area1 <= area2) { - console.log('lastDiv', lastDiv.offsetTop, lastDiv.offsetHeight); - return { - xStart: 0, - yStart: lastDiv.offsetTop + lastDiv.offsetHeight, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } else { - console.log('lastDiv', lastDiv.offsetTop); - return { - xStart: lastDiv.offsetLeft + lastDiv.offsetWidth, - yStart: lastDiv.offsetTop, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } - } else { - // Possible destinations: at the center bottom or at the right bottom. - const mainSectionChildren = Array.from(document.querySelectorAll('div.main-section > div').values()); - const sidebarChildren = Array.from(document.querySelectorAll('aside.sidebar > div').values()); - - // No presentation? Let's center on the screen - if (mainSectionChildren.length === 0) { - return { - xStart: 0, - yStart: 0, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } - - // At this point, we know we have at least one element in the main section. - const lastPresentationDiv = mainSectionChildren[mainSectionChildren.length-1]; - - const presentationArea = (game.offsetHeight - (lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight)) - * (lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth); - - let leftSideBar: number; - let bottomSideBar: number; - if (sidebarChildren.length === 0) { - leftSideBar = HtmlUtils.getElementByIdOrFail('sidebar').offsetLeft; - bottomSideBar = 0; - } else { - const lastSideBarChildren = sidebarChildren[sidebarChildren.length - 1]; - leftSideBar = lastSideBarChildren.offsetLeft; - bottomSideBar = lastSideBarChildren.offsetTop + lastSideBarChildren.offsetHeight; - } - const sideBarArea = (game.offsetWidth - leftSideBar) - * (game.offsetHeight - bottomSideBar); - - if (presentationArea <= sideBarArea) { - return { - xStart: leftSideBar, - yStart: bottomSideBar, - xEnd: game.offsetWidth, - yEnd: game.offsetHeight - } - } else { - return { - xStart: 0, - yStart: lastPresentationDiv.offsetTop + lastPresentationDiv.offsetHeight, - xEnd: /*lastPresentationDiv.offsetLeft + lastPresentationDiv.offsetWidth*/ game.offsetWidth , // To avoid flickering when a chat start, we center on the center of the screen, not the center of the main content area - yEnd: game.offsetHeight - } - } - } - } - public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager){ //delete previous element this.removeActionButton(id, userInputManager); diff --git a/front/src/WebRtc/MediaManager.ts b/front/src/WebRtc/MediaManager.ts index fb85252f..faa5edf7 100644 --- a/front/src/WebRtc/MediaManager.ts +++ b/front/src/WebRtc/MediaManager.ts @@ -7,7 +7,7 @@ import type { UserSimplePeerInterface } from "./SimplePeer"; import { SoundMeter } from "../Phaser/Components/SoundMeter"; import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable"; import { - gameOverlayVisibilityStore, localStreamStore, + localStreamStore, } from "../Stores/MediaStore"; import { screenSharingLocalStreamStore @@ -17,20 +17,13 @@ import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore" export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void; export type StartScreenSharingCallback = (media: MediaStream) => void; export type StopScreenSharingCallback = (media: MediaStream) => void; -export type ReportCallback = (message: string) => void; -export type ShowReportCallBack = (userId: string, userName: string | undefined) => void; -export type HelpCameraSettingsCallBack = () => void; import {cowebsiteCloseButtonId} from "./CoWebsiteManager"; +import {gameOverlayVisibilityStore} from "../Stores/GameOverlayStoreVisibility"; export class MediaManager { - private remoteVideo: Map = new Map(); - //FIX ME SOUNDMETER: check stalability of sound meter calculation - //mySoundMeterElement: HTMLDivElement; - - startScreenSharingCallBacks: Set = new Set(); - stopScreenSharingCallBacks: Set = new Set(); - showReportModalCallBacks: Set = new Set(); + startScreenSharingCallBacks : Set = new Set(); + stopScreenSharingCallBacks : Set = new Set(); @@ -40,21 +33,8 @@ export class MediaManager { private userInputManager?: UserInputManager; - //FIX ME SOUNDMETER: check stalability of sound meter calculation - /*private mySoundMeter?: SoundMeter|null; - private soundMeters: Map = new Map(); - private soundMeterElements: Map = new Map();*/ - constructor() { - this.pingCameraStatus(); - - //FIX ME SOUNDMETER: check stability of sound meter calculation - /*this.mySoundMeterElement = (HtmlUtils.getElementByIdOrFail('mySoundMeter')); - this.mySoundMeterElement.childNodes.forEach((value: ChildNode, index) => { - this.mySoundMeterElement.children.item(index)?.classList.remove('active'); - });*/ - //Check of ask notification navigator permission this.getNotification(); @@ -68,7 +48,6 @@ export class MediaManager { } }); - let isScreenSharing = false; screenSharingLocalStreamStore.subscribe((result) => { if (result.type === 'error') { console.error(result.error); @@ -77,32 +56,7 @@ export class MediaManager { }, this.userInputManager); return; } - - if (result.stream !== null) { - isScreenSharing = true; - this.addScreenSharingActiveVideo('me', DivImportance.Normal); - HtmlUtils.getElementByIdOrFail('screen-sharing-me').srcObject = result.stream; - } else { - if (isScreenSharing) { - isScreenSharing = false; - this.removeActiveScreenSharingVideo('me'); - } - } - }); - - /*screenSharingAvailableStore.subscribe((available) => { - if (available) { - document.querySelector('.btn-monitor')?.classList.remove('hide'); - } else { - document.querySelector('.btn-monitor')?.classList.add('hide'); - } - });*/ - } - - public updateScene(){ - //FIX ME SOUNDMETER: check stability of sound meter calculation - //this.updateSoudMeter(); } public showGameOverlay(): void { @@ -137,71 +91,6 @@ export class MediaManager { gameOverlayVisibilityStore.hideGameOverlay(); } - - - addActiveVideo(user: UserSimplePeerInterface, userName: string = "") { - - const userId = '' + user.userId - - userName = userName.toUpperCase(); - const color = this.getColorByString(userName); - - const html = ` -
-
- - ${userName} - - - - -
- - - - - -
-
- `; - - layoutManager.add(DivImportance.Normal, userId, html); - - this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail(userId)); - - //permit to create participant in discussion part - const showReportUser = () => { - for (const callBack of this.showReportModalCallBacks) { - callBack(userId, userName); - } - }; - this.addNewParticipant(userId, userName, undefined, showReportUser); - - const reportBanUserActionEl: HTMLImageElement = HtmlUtils.getElementByIdOrFail(`report-${userId}`); - reportBanUserActionEl.addEventListener('click', (e) => { - e.preventDefault(); - e.stopPropagation(); - showReportUser(); - }); - } - - addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){ - - userId = this.getScreenSharingId(userId); - const html = ` -
- -
- `; - - layoutManager.add(divImportance, userId, html); - - this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail(userId)); - } - private getScreenSharingId(userId: string): string { return `screen-sharing-${userId}`; } @@ -248,61 +137,6 @@ export class MediaManager { const blockLogoElement = HtmlUtils.getElementByIdOrFail('blocking-' + userId); show ? blockLogoElement.classList.add('active') : blockLogoElement.classList.remove('active'); } - addStreamRemoteVideo(userId: string, stream: MediaStream): void { - const remoteVideo = this.remoteVideo.get(userId); - if (remoteVideo === undefined) { - throw `Unable to find video for ${userId}`; - } - remoteVideo.srcObject = stream; - - //FIX ME SOUNDMETER: check stalability of sound meter calculation - //sound metter - /*const soundMeter = new SoundMeter(); - soundMeter.connectToSource(stream, new AudioContext()); - this.soundMeters.set(userId, soundMeter); - this.soundMeterElements.set(userId, HtmlUtils.getElementByIdOrFail('soundMeter-'+userId));*/ - } - addStreamRemoteScreenSharing(userId: string, stream: MediaStream) { - // In the case of screen sharing (going both ways), we may need to create the HTML element if it does not exist yet - const remoteVideo = this.remoteVideo.get(this.getScreenSharingId(userId)); - if (remoteVideo === undefined) { - this.addScreenSharingActiveVideo(userId); - } - - this.addStreamRemoteVideo(this.getScreenSharingId(userId), stream); - } - - removeActiveVideo(userId: string) { - layoutManager.remove(userId); - this.remoteVideo.delete(userId); - - //FIX ME SOUNDMETER: check stalability of sound meter calculation - /*this.soundMeters.get(userId)?.stop(); - this.soundMeters.delete(userId); - this.soundMeterElements.delete(userId);*/ - - //permit to remove user in discussion part - this.removeParticipant(userId); - } - removeActiveScreenSharingVideo(userId: string) { - this.removeActiveVideo(this.getScreenSharingId(userId)) - } - - isConnecting(userId: string): void { - const connectingSpinnerDiv = this.getSpinner(userId); - if (connectingSpinnerDiv === null) { - return; - } - connectingSpinnerDiv.style.display = 'block'; - } - - isConnected(userId: string): void { - const connectingSpinnerDiv = this.getSpinner(userId); - if (connectingSpinnerDiv === null) { - return; - } - connectingSpinnerDiv.style.display = 'none'; - } isError(userId: string): void { console.info("isError", `div-${userId}`); @@ -326,33 +160,11 @@ export class MediaManager { if (!element) { return null; } - const connnectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement | null; - return connnectingSpinnerDiv; + const connectingSpinnerDiv = element.getElementsByClassName('connecting-spinner').item(0) as HTMLDivElement|null; + return connectingSpinnerDiv; } - private getColorByString(str: String): String | null { - let hash = 0; - if (str.length === 0) return null; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - hash = hash & hash; - } - let color = '#'; - for (let i = 0; i < 3; i++) { - const value = (hash >> (i * 8)) & 255; - color += ('00' + value.toString(16)).substr(-2); - } - return color; - } - - public addNewParticipant(userId: number | string, name: string | undefined, img?: string, showReportUserCallBack?: ShowReportCallBack) { - discussionManager.addParticipant(userId, name, img, false, showReportUserCallBack); - } - - public removeParticipant(userId: number | string) { - discussionManager.removeParticipant(userId); - } - public addTriggerCloseJitsiFrameButton(id: String, Function: Function) { + public addTriggerCloseJitsiFrameButton(id: String, Function: Function){ this.triggerCloseJistiFrame.set(id, Function); } @@ -365,16 +177,6 @@ export class MediaManager { callback(); } } - /** - * For some reasons, the microphone muted icon or the stream is not always up to date. - * Here, every 30 seconds, we are "reseting" the streams and sending again the constraints to the other peers via the data channel again (see SimplePeer::pushVideoToRemoteUser) - **/ - private pingCameraStatus() { - /*setInterval(() => { - console.log('ping camera status'); - this.triggerUpdatedLocalStreamCallbacks(this.localStream); - }, 30000);*/ - } public addNewMessage(name: string, message: string, isMe: boolean = false) { discussionManager.addMessage(name, message, isMe); @@ -389,61 +191,11 @@ export class MediaManager { discussionManager.onSendMessageCallback(userId, callback); } - get activatedDiscussion() { - return discussionManager.activatedDiscussion; - } - - public setUserInputManager(userInputManager: UserInputManager) { + public setUserInputManager(userInputManager : UserInputManager){ this.userInputManager = userInputManager; discussionManager.setUserInputManager(userInputManager); } - public setShowReportModalCallBacks(callback: ShowReportCallBack) { - this.showReportModalCallBacks.add(callback); - } - - //FIX ME SOUNDMETER: check stalability of sound meter calculation - /*updateSoudMeter(){ - try{ - const volume = parseInt(((this.mySoundMeter ? this.mySoundMeter.getVolume() : 0) / 10).toFixed(0)); - this.setVolumeSoundMeter(volume, this.mySoundMeterElement); - - for(const indexUserId of this.soundMeters.keys()){ - const soundMeter = this.soundMeters.get(indexUserId); - const soundMeterElement = this.soundMeterElements.get(indexUserId); - if (!soundMeter || !soundMeterElement) { - return; - } - const volumeByUser = parseInt((soundMeter.getVolume() / 10).toFixed(0)); - this.setVolumeSoundMeter(volumeByUser, soundMeterElement); - } - } catch (err) { - //console.error(err); - } - }*/ - - private setVolumeSoundMeter(volume: number, element: HTMLDivElement) { - if (volume <= 0 && !element.classList.contains('active')) { - return; - } - element.classList.remove('active'); - if (volume <= 0) { - return; - } - element.classList.add('active'); - element.childNodes.forEach((value: ChildNode, index) => { - const elementChildren = element.children.item(index); - if (!elementChildren) { - return; - } - elementChildren.classList.remove('active'); - if ((index + 1) > volume) { - return; - } - elementChildren.classList.add('active'); - }); - } - public getNotification(){ //Get notification if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") { diff --git a/front/src/WebRtc/ScreenSharingPeer.ts b/front/src/WebRtc/ScreenSharingPeer.ts index d797f59b..be534276 100644 --- a/front/src/WebRtc/ScreenSharingPeer.ts +++ b/front/src/WebRtc/ScreenSharingPeer.ts @@ -1,9 +1,11 @@ import type * as SimplePeerNamespace from "simple-peer"; import {mediaManager} from "./MediaManager"; -import {STUN_SERVER, TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable"; +import {STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER} from "../Enum/EnvironmentVariable"; import type {RoomConnection} from "../Connexion/RoomConnection"; -import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer"; +import {MESSAGE_TYPE_CONSTRAINT, PeerStatus} from "./VideoPeer"; import type {UserSimplePeerInterface} from "./SimplePeer"; +import {Readable, readable, writable, Writable} from "svelte/store"; +import {videoFocusStore} from "../Stores/VideoFocusStore"; const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); @@ -17,9 +19,12 @@ export class ScreenSharingPeer extends Peer { private isReceivingStream:boolean = false; public toClose: boolean = false; public _connected: boolean = false; - private userId: number; + public readonly userId: number; + public readonly uniqueId: string; + public readonly streamStore: Readable; + public readonly statusStore: Readable; - constructor(user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection, stream: MediaStream | null) { + constructor(user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, stream: MediaStream | null) { super({ initiator: initiator ? initiator : false, //reconnectTimer: 10000, @@ -38,6 +43,55 @@ export class ScreenSharingPeer extends Peer { }); this.userId = user.userId; + this.uniqueId = 'screensharing_'+this.userId; + + this.streamStore = readable(null, (set) => { + const onStream = (stream: MediaStream|null) => { + videoFocusStore.focus(this); + set(stream); + }; + const onData = (chunk: Buffer) => { + // We unfortunately need to rely on an event to let the other party know a stream has stopped. + // It seems there is no native way to detect that. + // TODO: we might rely on the "ended" event: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/ended_event + const message = JSON.parse(chunk.toString('utf8')); + if (message.streamEnded !== true) { + console.error('Unexpected message on screen sharing peer connection'); + return; + } + set(null); + } + + this.on('stream', onStream); + this.on('data', onData); + + return () => { + this.off('stream', onStream); + this.off('data', onData); + }; + }); + + this.statusStore = readable("connecting", (set) => { + const onConnect = () => { + set('connected'); + }; + const onError = () => { + set('error'); + }; + const onClose = () => { + set('closed'); + }; + + this.on('connect', onConnect); + this.on('error', onError); + this.on('close', onClose); + + return () => { + this.off('connect', onConnect); + this.off('error', onError); + this.off('close', onClose); + }; + }); //start listen signal for the peer connection this.on('signal', (data: unknown) => { @@ -54,27 +108,13 @@ export class ScreenSharingPeer extends Peer { this.destroy(); }); - this.on('data', (chunk: Buffer) => { - // We unfortunately need to rely on an event to let the other party know a stream has stopped. - // It seems there is no native way to detect that. - const message = JSON.parse(chunk.toString('utf8')); - if (message.streamEnded !== true) { - console.error('Unexpected message on screen sharing peer connection'); - return; - } - mediaManager.removeActiveScreenSharingVideo("" + this.userId); - }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any this.on('error', (err: any) => { console.error(`screen sharing error => ${this.userId} => ${err.code}`, err); - //mediaManager.isErrorScreenSharing(this.userId); }); this.on('connect', () => { this._connected = true; - // FIXME: we need to put the loader on the screen sharing connection - mediaManager.isConnected("" + this.userId); console.info(`connect => ${this.userId}`); }); @@ -88,7 +128,6 @@ export class ScreenSharingPeer extends Peer { } private sendWebrtcScreenSharingSignal(data: unknown) { - //console.log("sendWebrtcScreenSharingSignal", data); try { this.connection.sendWebrtcScreenSharingSignal(data, this.userId); }catch (e) { @@ -100,13 +139,9 @@ export class ScreenSharingPeer extends Peer { * Sends received stream to screen. */ private stream(stream?: MediaStream) { - //console.log(`ScreenSharingPeer::stream => ${this.userId}`, stream); - //console.log(`stream => ${this.userId} => `, stream); if(!stream){ - mediaManager.removeActiveScreenSharingVideo("" + this.userId); this.isReceivingStream = false; } else { - mediaManager.addStreamRemoteScreenSharing("" + this.userId, stream); this.isReceivingStream = true; } } @@ -121,7 +156,6 @@ export class ScreenSharingPeer extends Peer { if(!this.toClose){ return; } - mediaManager.removeActiveScreenSharingVideo("" + this.userId); // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. //console.log('Closing connection with '+userId); diff --git a/front/src/WebRtc/SimplePeer.ts b/front/src/WebRtc/SimplePeer.ts index 2a502bab..ecc0e21b 100644 --- a/front/src/WebRtc/SimplePeer.ts +++ b/front/src/WebRtc/SimplePeer.ts @@ -6,19 +6,15 @@ import { mediaManager, StartScreenSharingCallback, StopScreenSharingCallback, - UpdatedLocalStreamCallback } from "./MediaManager"; import {ScreenSharingPeer} from "./ScreenSharingPeer"; import {MESSAGE_TYPE_BLOCKED, MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer"; import type {RoomConnection} from "../Connexion/RoomConnection"; -import {connectionManager} from "../Connexion/ConnectionManager"; -import {GameConnexionTypes} from "../Url/UrlManager"; import {blackListManager} from "./BlackListManager"; import {get} from "svelte/store"; import {localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore} from "../Stores/MediaStore"; import {screenSharingLocalStreamStore} from "../Stores/ScreenSharingStore"; -import {DivImportance, layoutManager} from "./LayoutManager"; -import {HtmlUtils} from "./HtmlUtils"; +import {discussionManager} from "./DiscussionManager"; export interface UserSimplePeerInterface{ userId: number; @@ -28,8 +24,10 @@ export interface UserSimplePeerInterface{ webRtcPassword?: string|undefined; } +export type RemotePeer = VideoPeer | ScreenSharingPeer; + export interface PeerConnectionListener { - onConnect(user: UserSimplePeerInterface): void; + onConnect(user: RemotePeer): void; onDisconnect(userId: number): void; } @@ -124,7 +122,6 @@ export class SimplePeer { // This would be symmetrical to the way we handle disconnection. //start connection - //console.log('receiveWebrtcStart. Initiator: ', user.initiator) if(!user.initiator){ return; } @@ -159,20 +156,15 @@ export class SimplePeer { let name = user.name; if (!name) { - const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === user.userId); - if (userSearch) { - name = userSearch.name; - } + name = this.getName(user.userId); } - mediaManager.removeActiveVideo("" + user.userId); - - mediaManager.addActiveVideo(user, name); + discussionManager.removeParticipant(user.userId); this.lastWebrtcUserName = user.webRtcUser; this.lastWebrtcPassword = user.webRtcPassword; - const peer = new VideoPeer(user, user.initiator ? user.initiator : false, this.Connection, localStream); + const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream); //permit to send message mediaManager.addSendMessageCallback(user.userId,(message: string) => { @@ -196,11 +188,20 @@ export class SimplePeer { this.PeerConnectionArray.set(user.userId, peer); for (const peerConnectionListener of this.peerConnectionListeners) { - peerConnectionListener.onConnect(user); + peerConnectionListener.onConnect(peer); } return peer; } + private getName(userId: number): string { + const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === userId); + if (userSearch) { + return userSearch.name || ''; + } else { + return ''; + } + } + /** * create peer connection to bind users */ @@ -221,23 +222,19 @@ export class SimplePeer { return null; } - // We should display the screen sharing ONLY if we are not initiator - if (!user.initiator) { - mediaManager.removeActiveScreenSharingVideo("" + user.userId); - mediaManager.addScreenSharingActiveVideo("" + user.userId); - } - // Enrich the user with last known credentials (if they are not set in the user object, which happens when a user triggers the screen sharing) if (user.webRtcUser === undefined) { user.webRtcUser = this.lastWebrtcUserName; user.webRtcPassword = this.lastWebrtcPassword; } - const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, this.Connection, stream); + const name = this.getName(user.userId); + + const peer = new ScreenSharingPeer(user, user.initiator ? user.initiator : false, name, this.Connection, stream); this.PeerScreenSharingConnectionArray.set(user.userId, peer); for (const peerConnectionListener of this.peerConnectionListeners) { - peerConnectionListener.onConnect(user); + peerConnectionListener.onConnect(peer); } return peer; } @@ -288,7 +285,7 @@ export class SimplePeer { */ private closeScreenSharingConnection(userId : number) { try { - mediaManager.removeActiveScreenSharingVideo("" + userId); + //mediaManager.removeActiveScreenSharingVideo("" + userId); const peer = this.PeerScreenSharingConnectionArray.get(userId); if (peer === undefined) { console.warn("closeScreenSharingConnection => Tried to close connection for user "+userId+" but could not find user") diff --git a/front/src/WebRtc/VideoPeer.ts b/front/src/WebRtc/VideoPeer.ts index 5ca8952c..5b5212b9 100644 --- a/front/src/WebRtc/VideoPeer.ts +++ b/front/src/WebRtc/VideoPeer.ts @@ -5,11 +5,14 @@ import type {RoomConnection} from "../Connexion/RoomConnection"; import {blackListManager} from "./BlackListManager"; import type {Subscription} from "rxjs"; import type {UserSimplePeerInterface} from "./SimplePeer"; -import {get} from "svelte/store"; +import {get, readable, Readable} from "svelte/store"; import {obtainedMediaConstraintStore} from "../Stores/MediaStore"; +import {discussionManager} from "./DiscussionManager"; const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); +export type PeerStatus = "connecting" | "connected" | "error" | "closed"; + export const MESSAGE_TYPE_CONSTRAINT = 'constraint'; export const MESSAGE_TYPE_MESSAGE = 'message'; export const MESSAGE_TYPE_BLOCKED = 'blocked'; @@ -22,12 +25,15 @@ export class VideoPeer extends Peer { public _connected: boolean = false; private remoteStream!: MediaStream; private blocked: boolean = false; - private userId: number; - private userName: string; + public readonly userId: number; + public readonly uniqueId: string; private onBlockSubscribe: Subscription; private onUnBlockSubscribe: Subscription; + public readonly streamStore: Readable; + public readonly statusStore: Readable; + public readonly constraintsStore: Readable; - constructor(public user: UserSimplePeerInterface, initiator: boolean, private connection: RoomConnection, localStream: MediaStream | null) { + constructor(public user: UserSimplePeerInterface, initiator: boolean, public readonly userName: string, private connection: RoomConnection, localStream: MediaStream | null) { super({ initiator: initiator ? initiator : false, //reconnectTimer: 10000, @@ -46,7 +52,68 @@ export class VideoPeer extends Peer { }); this.userId = user.userId; - this.userName = user.name || ''; + this.uniqueId = 'video_'+this.userId; + + this.streamStore = readable(null, (set) => { + const onStream = (stream: MediaStream|null) => { + set(stream); + }; + const onData = (chunk: Buffer) => { + this.on('data', (chunk: Buffer) => { + const message = JSON.parse(chunk.toString('utf8')); + if (message.type === MESSAGE_TYPE_CONSTRAINT) { + if (!message.video) { + set(null); + } + } + }); + } + + this.on('stream', onStream); + this.on('data', onData); + + return () => { + this.off('stream', onStream); + this.off('data', onData); + }; + }); + + this.constraintsStore = readable(null, (set) => { + const onData = (chunk: Buffer) => { + const message = JSON.parse(chunk.toString('utf8')); + if(message.type === MESSAGE_TYPE_CONSTRAINT) { + set(message); + } + } + + this.on('data', onData); + + return () => { + this.off('data', onData); + }; + }); + + this.statusStore = readable("connecting", (set) => { + const onConnect = () => { + set('connected'); + }; + const onError = () => { + set('error'); + }; + const onClose = () => { + set('closed'); + }; + + this.on('connect', onConnect); + this.on('error', onError); + this.on('close', onClose); + + return () => { + this.off('connect', onConnect); + this.off('error', onError); + this.off('close', onClose); + }; + }); //start listen signal for the peer connection this.on('signal', (data: unknown) => { @@ -69,8 +136,6 @@ export class VideoPeer extends Peer { this.on('connect', () => { this._connected = true; - mediaManager.isConnected("" + this.userId); - console.info(`connect => ${this.userId}`); }); this.on('data', (chunk: Buffer) => { @@ -152,7 +217,6 @@ export class VideoPeer extends Peer { if (blackListManager.isBlackListed(this.userId) || this.blocked) { this.toggleRemoteStream(false); } - mediaManager.addStreamRemoteVideo("" + this.userId, stream); }catch (err){ console.error(err); } @@ -169,7 +233,7 @@ export class VideoPeer extends Peer { } this.onBlockSubscribe.unsubscribe(); this.onUnBlockSubscribe.unsubscribe(); - mediaManager.removeActiveVideo("" + this.userId); + discussionManager.removeParticipant(this.userId); // FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray" // I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel. super.destroy(error); diff --git a/front/style/style.scss b/front/style/style.scss index 5f2e48b2..5d33fbb3 100644 --- a/front/style/style.scss +++ b/front/style/style.scss @@ -39,102 +39,93 @@ body .message-info.warning { .video-container { position: relative; transition: all 0.2s ease; - height: 100%; -} + background-color: #00000099; -.video-container.nes-container.is-dark { - padding: 12px 12px !important; -} + video { + width: 100%; + height: 100%; + max-height: 90vh; + } -.video-container i { - position: absolute; - width: 100px; - height: 100px; - left: calc(50% - 50px); - top: calc(50% - 50px); - background-color: black; - border-radius: 50%; - text-align: center; - padding-top: 32px; - font-size: 28px; - color: white; - overflow: hidden; -} + i { + position: absolute; + width: 100px; + height: 100px; + left: calc(50% - 50px); + top: calc(50% - 50px); + text-align: center; + padding-top: 32px; + font-size: 28px; + color: white; + overflow: hidden; + } -.video-container img { - position: absolute; - display: none; - width: 40px; - height: 40px; - left: 5px; - bottom: 5px; - padding: 10px; - z-index: 2; -} + img { + position: absolute; + display: none; + width: 40px; + height: 40px; + left: 5px; + bottom: 5px; + padding: 10px; + z-index: 2; + } -.video-container img.block-logo { - left: 30%; - bottom: 15%; - width: 150px; - height: 150px; -} + img.block-logo { + left: 30%; + bottom: 15%; + width: 150px; + height: 150px; + } -.video-container button.report { - display: block; - background: none; - background-color: rgba(0, 0, 0, 0); - border: none; - background-color: black; - border-radius: 15px; - position: absolute; - width: 0px; - height: 35px; - right: 5px; - bottom: 5px; - padding: 0px; - overflow: hidden; - z-index: 2; - transition: all .5s ease; -} + button.report{ + display: block; + position: absolute; + width: 0px; + height: 35px; + right: 5px; + bottom: 5px; + padding: 0px; + overflow: hidden; + z-index: 2; + transition: all .5s ease; -.video-container:hover button.report { - width: 35px; - padding: 10px; -} + img{ + position: absolute; + display: block; + bottom: 5px; + left: 5px; + margin: 0; + padding: 0; + width: 25px; + height: 25px; + } -.video-container button.report:hover { - width: 160px; -} + span { + position: absolute; + bottom: 6px; + left: 36px; + color: white; + font-size: 16px; + } -.video-container button.report img { - position: absolute; - display: block; - bottom: 5px; - left: 5px; - margin: 0; - padding: 0; - width: 25px; - height: 25px; -} + img.active { + display: block !important; + } + } -.video-container button.report span { - position: absolute; - bottom: 6px; - left: 36px; - color: white; - font-size: 16px; -} + &:hover button.report{ + width: 35px; + padding: 10px; -.video-container img.active { - display: block !important; -} + &:hover { + width: 160px; + } + } -.video-container video { - height: 100%; -} - -.video-container video:focus { - outline: none; + video:focus{ + outline: none; + } } .video-container.div-myCamVideo{ @@ -213,7 +204,7 @@ video.myCamVideo{ display: inline-flex; bottom: 10px; right: 15px; - width: 180px; + width: 240px; height: 40px; text-align: center; align-content: center; @@ -230,8 +221,7 @@ video.myCamVideo{ justify-content: center; width: 44px; height: 44px; - width: auto; - transform: translateY(20px); + transform: translateY(15px); transition-timing-function: ease-in-out; margin: 0 4%; } @@ -277,6 +267,16 @@ video.myCamVideo{ .btn-cam-action:hover .btn-monitor.hide{ transform: translateY(60px); } +.btn-layout{ + pointer-events: auto; + transition: all .15s; +} +.btn-layout.hide { + transform: translateY(60px); +} +.btn-cam-action:hover .btn-layout.hide{ + transform: translateY(60px); +} .btn-copy{ pointer-events: auto; transition: all .3s; diff --git a/front/webpack.config.ts b/front/webpack.config.ts index 34e646a1..71eb8b91 100644 --- a/front/webpack.config.ts +++ b/front/webpack.config.ts @@ -95,6 +95,7 @@ module.exports = { if (warning.code === 'a11y-no-onchange') { return } if (warning.code === 'a11y-autofocus') { return } + if (warning.code === 'a11y-media-has-caption') { return } // process as usual handleWarning(warning); diff --git a/pusher/src/App.ts b/pusher/src/App.ts index 7a272404..81aed045 100644 --- a/pusher/src/App.ts +++ b/pusher/src/App.ts @@ -1,11 +1,11 @@ // lib/app.ts -import {IoSocketController} from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..." -import {AuthenticateController} from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..." -import {MapController} from "./Controller/MapController"; -import {PrometheusController} from "./Controller/PrometheusController"; -import {DebugController} from "./Controller/DebugController"; -import {App as uwsApp} from "./Server/sifrr.server"; -import {AdminController} from "./Controller/AdminController"; +import { IoSocketController } from "./Controller/IoSocketController"; //TODO fix import by "_Controller/..." +import { AuthenticateController } from "./Controller/AuthenticateController"; //TODO fix import by "_Controller/..." +import { MapController } from "./Controller/MapController"; +import { PrometheusController } from "./Controller/PrometheusController"; +import { DebugController } from "./Controller/DebugController"; +import { App as uwsApp } from "./Server/sifrr.server"; +import { AdminController } from "./Controller/AdminController"; class App { public app: uwsApp; diff --git a/pusher/src/Controller/AdminController.ts b/pusher/src/Controller/AdminController.ts index 74d4e792..ec1bd067 100644 --- a/pusher/src/Controller/AdminController.ts +++ b/pusher/src/Controller/AdminController.ts @@ -1,19 +1,21 @@ -import {BaseController} from "./BaseController"; -import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; -import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; -import {apiClientRepository} from "../Services/ApiClientRepository"; -import {AdminRoomMessage, WorldFullWarningToRoomMessage, RefreshRoomPromptMessage} from "../Messages/generated/messages_pb"; +import { BaseController } from "./BaseController"; +import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js"; +import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable"; +import { apiClientRepository } from "../Services/ApiClientRepository"; +import { + AdminRoomMessage, + WorldFullWarningToRoomMessage, + RefreshRoomPromptMessage, +} from "../Messages/generated/messages_pb"; - -export class AdminController extends BaseController{ - - constructor(private App : TemplatedApp) { +export class AdminController extends BaseController { + constructor(private App: TemplatedApp) { super(); this.App = App; this.receiveGlobalMessagePrompt(); this.receiveRoomEditionPrompt(); } - + receiveRoomEditionPrompt() { this.App.options("/room/refresh", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); @@ -23,25 +25,25 @@ export class AdminController extends BaseController{ // eslint-disable-next-line @typescript-eslint/no-misused-promises this.App.post("/room/refresh", async (res: HttpResponse, req: HttpRequest) => { res.onAborted(() => { - console.warn('/message request was aborted'); - }) + console.warn("/message request was aborted"); + }); - const token = req.getHeader('admin-token'); + const token = req.getHeader("admin-token"); const body = await res.json(); if (token !== ADMIN_API_TOKEN) { - console.error('Admin access refused for token: '+token) - res.writeStatus("401 Unauthorized").end('Incorrect token'); + console.error("Admin access refused for token: " + token); + res.writeStatus("401 Unauthorized").end("Incorrect token"); return; } try { - if (typeof body.roomId !== 'string') { - throw 'Incorrect roomId parameter' + if (typeof body.roomId !== "string") { + throw "Incorrect roomId parameter"; } const roomId: string = body.roomId; - await apiClientRepository.getClient(roomId).then((roomClient) =>{ + await apiClientRepository.getClient(roomId).then((roomClient) => { return new Promise((res, rej) => { const roomMessage = new RefreshRoomPromptMessage(); roomMessage.setRoomid(roomId); @@ -57,12 +59,10 @@ export class AdminController extends BaseController{ } res.writeStatus("200"); - res.end('ok'); - - + res.end("ok"); }); } - + receiveGlobalMessagePrompt() { this.App.options("/message", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); @@ -71,59 +71,57 @@ export class AdminController extends BaseController{ // eslint-disable-next-line @typescript-eslint/no-misused-promises this.App.post("/message", async (res: HttpResponse, req: HttpRequest) => { - res.onAborted(() => { - console.warn('/message request was aborted'); - }) + console.warn("/message request was aborted"); + }); - - const token = req.getHeader('admin-token'); + const token = req.getHeader("admin-token"); const body = await res.json(); - + if (token !== ADMIN_API_TOKEN) { - console.error('Admin access refused for token: '+token) - res.writeStatus("401 Unauthorized").end('Incorrect token'); + console.error("Admin access refused for token: " + token); + res.writeStatus("401 Unauthorized").end("Incorrect token"); return; } try { - if (typeof body.text !== 'string') { - throw 'Incorrect text parameter' + if (typeof body.text !== "string") { + throw "Incorrect text parameter"; } - if (body.type !== 'capacity' && body.type !== 'message') { - throw 'Incorrect type parameter' + if (body.type !== "capacity" && body.type !== "message") { + throw "Incorrect type parameter"; } - if (!body.targets || typeof body.targets !== 'object') { - throw 'Incorrect targets parameter' + if (!body.targets || typeof body.targets !== "object") { + throw "Incorrect targets parameter"; } const text: string = body.text; const type: string = body.type; const targets: string[] = body.targets; - await Promise.all(targets.map((roomId) => { - return apiClientRepository.getClient(roomId).then((roomClient) =>{ - return new Promise((res, rej) => { - if (type === 'message') { - const roomMessage = new AdminRoomMessage(); - roomMessage.setMessage(text); - roomMessage.setRoomid(roomId); + await Promise.all( + targets.map((roomId) => { + return apiClientRepository.getClient(roomId).then((roomClient) => { + return new Promise((res, rej) => { + if (type === "message") { + const roomMessage = new AdminRoomMessage(); + roomMessage.setMessage(text); + roomMessage.setRoomid(roomId); - roomClient.sendAdminMessageToRoom(roomMessage, (err) => { - err ? rej(err) : res(); - }); - } else if (type === 'capacity') { - const roomMessage = new WorldFullWarningToRoomMessage(); - roomMessage.setRoomid(roomId); - - roomClient.sendWorldFullWarningToRoom(roomMessage, (err) => { - err ? rej(err) : res(); - }); - } + roomClient.sendAdminMessageToRoom(roomMessage, (err) => { + err ? rej(err) : res(); + }); + } else if (type === "capacity") { + const roomMessage = new WorldFullWarningToRoomMessage(); + roomMessage.setRoomid(roomId); + roomClient.sendWorldFullWarningToRoom(roomMessage, (err) => { + err ? rej(err) : res(); + }); + } + }); }); - }); - })); - + }) + ); } catch (err) { this.errorToResponse(err, res); return; @@ -131,7 +129,7 @@ export class AdminController extends BaseController{ res.writeStatus("200"); this.addCorsHeaders(res); - res.end('ok'); + res.end("ok"); }); } } diff --git a/pusher/src/Controller/AuthenticateController.ts b/pusher/src/Controller/AuthenticateController.ts index 317848c0..3012e275 100644 --- a/pusher/src/Controller/AuthenticateController.ts +++ b/pusher/src/Controller/AuthenticateController.ts @@ -1,17 +1,16 @@ -import { v4 } from 'uuid'; -import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; -import {BaseController} from "./BaseController"; -import {adminApi} from "../Services/AdminApi"; -import {jwtTokenManager} from "../Services/JWTTokenManager"; -import {parse} from "query-string"; +import { v4 } from "uuid"; +import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js"; +import { BaseController } from "./BaseController"; +import { adminApi } from "../Services/AdminApi"; +import { jwtTokenManager } from "../Services/JWTTokenManager"; +import { parse } from "query-string"; export interface TokenInterface { - userUuid: string + userUuid: string; } export class AuthenticateController extends BaseController { - - constructor(private App : TemplatedApp) { + constructor(private App: TemplatedApp) { super(); this.register(); this.verify(); @@ -19,7 +18,7 @@ export class AuthenticateController extends BaseController { } //Try to login with an admin token - private register(){ + private register() { this.App.options("/register", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); @@ -29,15 +28,15 @@ export class AuthenticateController extends BaseController { this.App.post("/register", (res: HttpResponse, req: HttpRequest) => { (async () => { res.onAborted(() => { - console.warn('Login request was aborted'); - }) + console.warn("Login request was aborted"); + }); const param = await res.json(); //todo: what to do if the organizationMemberToken is already used? - const organizationMemberToken:string|null = param.organizationMemberToken; + const organizationMemberToken: string | null = param.organizationMemberToken; try { - if (typeof organizationMemberToken != 'string') throw new Error('No organization token'); + if (typeof organizationMemberToken != "string") throw new Error("No organization token"); const data = await adminApi.fetchMemberDataByToken(organizationMemberToken); const userUuid = data.userUuid; const organizationSlug = data.organizationSlug; @@ -49,28 +48,26 @@ export class AuthenticateController extends BaseController { const authToken = jwtTokenManager.createJWTToken(userUuid); res.writeStatus("200 OK"); this.addCorsHeaders(res); - res.end(JSON.stringify({ - authToken, - userUuid, - organizationSlug, - worldSlug, - roomSlug, - mapUrlStart, - organizationMemberToken, - textures - })); - + res.end( + JSON.stringify({ + authToken, + userUuid, + organizationSlug, + worldSlug, + roomSlug, + mapUrlStart, + organizationMemberToken, + textures, + }) + ); } catch (e) { this.errorToResponse(e, res); } - - })(); }); - } - private verify(){ + private verify() { this.App.options("/verify", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); @@ -82,50 +79,55 @@ export class AuthenticateController extends BaseController { const query = parse(req.getQuery()); res.onAborted(() => { - console.warn('verify request was aborted'); - }) + console.warn("verify request was aborted"); + }); try { await jwtTokenManager.getUserUuidFromToken(query.token as string); } catch (e) { res.writeStatus("400 Bad Request"); this.addCorsHeaders(res); - res.end(JSON.stringify({ - "success": false, - "message": "Invalid JWT token" - })); + res.end( + JSON.stringify({ + success: false, + message: "Invalid JWT token", + }) + ); return; } res.writeStatus("200 OK"); this.addCorsHeaders(res); - res.end(JSON.stringify({ - "success": true - })); + res.end( + JSON.stringify({ + success: true, + }) + ); })(); }); - } //permit to login on application. Return token to connect on Websocket IO. - private anonymLogin(){ + private anonymLogin() { this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { this.addCorsHeaders(res); res.end(); }); - this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { + this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { res.onAborted(() => { - console.warn('Login request was aborted'); - }) + console.warn("Login request was aborted"); + }); const userUuid = v4(); const authToken = jwtTokenManager.createJWTToken(userUuid); res.writeStatus("200 OK"); this.addCorsHeaders(res); - res.end(JSON.stringify({ - authToken, - userUuid, - })); + res.end( + JSON.stringify({ + authToken, + userUuid, + }) + ); }); } } diff --git a/pusher/src/Controller/BaseController.ts b/pusher/src/Controller/BaseController.ts index 91882138..ce378a55 100644 --- a/pusher/src/Controller/BaseController.ts +++ b/pusher/src/Controller/BaseController.ts @@ -1,11 +1,10 @@ -import {HttpResponse} from "uWebSockets.js"; - +import { HttpResponse } from "uWebSockets.js"; export class BaseController { protected addCorsHeaders(res: HttpResponse): void { - res.writeHeader('access-control-allow-headers', 'Origin, X-Requested-With, Content-Type, Accept'); - res.writeHeader('access-control-allow-methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); - res.writeHeader('access-control-allow-origin', '*'); + res.writeHeader("access-control-allow-headers", "Origin, X-Requested-With, Content-Type, Accept"); + res.writeHeader("access-control-allow-methods", "GET, POST, OPTIONS, PUT, PATCH, DELETE"); + res.writeHeader("access-control-allow-origin", "*"); } /** @@ -16,23 +15,23 @@ export class BaseController { if (e && e.message) { let url = e?.config?.url; if (url !== undefined) { - url = ' for URL: '+url; + url = " for URL: " + url; } else { - url = ''; + url = ""; } - console.error('ERROR: '+e.message+url); - } else if (typeof(e) === 'string') { + console.error("ERROR: " + e.message + url); + } else if (typeof e === "string") { console.error(e); } if (e.stack) { console.error(e.stack); } if (e.response) { - res.writeStatus(e.response.status+" "+e.response.statusText); + res.writeStatus(e.response.status + " " + e.response.statusText); this.addCorsHeaders(res); - res.end("An error occurred: "+e.response.status+" "+e.response.statusText); + res.end("An error occurred: " + e.response.status + " " + e.response.statusText); } else { - res.writeStatus("500 Internal Server Error") + res.writeStatus("500 Internal Server Error"); this.addCorsHeaders(res); res.end("An error occurred"); } diff --git a/pusher/src/Controller/DebugController.ts b/pusher/src/Controller/DebugController.ts index af2db139..0b0d188b 100644 --- a/pusher/src/Controller/DebugController.ts +++ b/pusher/src/Controller/DebugController.ts @@ -1,45 +1,46 @@ -import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; -import {IoSocketController} from "_Controller/IoSocketController"; -import {stringify} from "circular-json"; -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -import { parse } from 'query-string'; -import {App} from "../Server/sifrr.server"; -import {socketManager} from "../Services/SocketManager"; +import { ADMIN_API_TOKEN } from "../Enum/EnvironmentVariable"; +import { IoSocketController } from "_Controller/IoSocketController"; +import { stringify } from "circular-json"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; +import { parse } from "query-string"; +import { App } from "../Server/sifrr.server"; +import { socketManager } from "../Services/SocketManager"; export class DebugController { - constructor(private App : App) { + constructor(private App: App) { this.getDump(); } - - getDump(){ + getDump() { this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => { const query = parse(req.getQuery()); if (query.token !== ADMIN_API_TOKEN) { - return res.status(401).send('Invalid token sent!'); + return res.status(401).send("Invalid token sent!"); } - return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify( - socketManager.getWorlds(), - (key: unknown, value: unknown) => { - if(value instanceof Map) { - const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any - for (const [mapKey, mapValue] of value.entries()) { - obj[mapKey] = mapValue; - } - return obj; - } else if(value instanceof Set) { + return res + .writeStatus("200 OK") + .writeHeader("Content-Type", "application/json") + .end( + stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => { + if (value instanceof Map) { + const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any + for (const [mapKey, mapValue] of value.entries()) { + obj[mapKey] = mapValue; + } + return obj; + } else if (value instanceof Set) { const obj: Array = []; for (const [setKey, setValue] of value.entries()) { obj.push(setValue); } return obj; - } else { - return value; - } - } - )); + } else { + return value; + } + }) + ); }); } } diff --git a/pusher/src/Controller/IoSocketController.ts b/pusher/src/Controller/IoSocketController.ts index b2079953..1af9d917 100644 --- a/pusher/src/Controller/IoSocketController.ts +++ b/pusher/src/Controller/IoSocketController.ts @@ -1,6 +1,6 @@ -import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." -import {GameRoomPolicyTypes} from "../Model/PusherRoom"; -import {PointInterface} from "../Model/Websocket/PointInterface"; +import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." +import { GameRoomPolicyTypes } from "../Model/PusherRoom"; +import { PointInterface } from "../Model/Websocket/PointInterface"; import { SetPlayerDetailsMessage, SubMessage, @@ -18,17 +18,17 @@ import { CompanionMessage, EmotePromptMessage, } from "../Messages/generated/messages_pb"; -import {UserMovesMessage} from "../Messages/generated/messages_pb"; -import {TemplatedApp} from "uWebSockets.js" -import {parse} from "query-string"; -import {jwtTokenManager} from "../Services/JWTTokenManager"; -import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi"; -import {SocketManager, socketManager} from "../Services/SocketManager"; -import {emitInBatch} from "../Services/IoSocketHelpers"; -import {ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER} from "../Enum/EnvironmentVariable"; -import {Zone} from "_Model/Zone"; -import {ExAdminSocketInterface} from "_Model/Websocket/ExAdminSocketInterface"; -import {v4} from "uuid"; +import { UserMovesMessage } from "../Messages/generated/messages_pb"; +import { TemplatedApp } from "uWebSockets.js"; +import { parse } from "query-string"; +import { jwtTokenManager } from "../Services/JWTTokenManager"; +import { adminApi, CharacterTexture, FetchMemberDataByUuidResponse } from "../Services/AdminApi"; +import { SocketManager, socketManager } from "../Services/SocketManager"; +import { emitInBatch } from "../Services/IoSocketHelpers"; +import { ADMIN_API_TOKEN, ADMIN_API_URL, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable"; +import { Zone } from "_Model/Zone"; +import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface"; +import { v4 } from "uuid"; export class IoSocketController { private nextUserId: number = 1; @@ -39,32 +39,29 @@ export class IoSocketController { } adminRoomSocket() { - this.app.ws('/admin/rooms', { + this.app.ws("/admin/rooms", { upgrade: (res, req, context) => { const query = parse(req.getQuery()); - const websocketKey = req.getHeader('sec-websocket-key'); - const websocketProtocol = req.getHeader('sec-websocket-protocol'); - const websocketExtensions = req.getHeader('sec-websocket-extensions'); + const websocketKey = req.getHeader("sec-websocket-key"); + const websocketProtocol = req.getHeader("sec-websocket-protocol"); + const websocketExtensions = req.getHeader("sec-websocket-extensions"); const token = query.token; if (token !== ADMIN_API_TOKEN) { - console.log('Admin access refused for token: '+token) - res.writeStatus("401 Unauthorized").end('Incorrect token'); + console.log("Admin access refused for token: " + token); + res.writeStatus("401 Unauthorized").end("Incorrect token"); return; } const roomId = query.roomId; - if (typeof roomId !== 'string') { - console.error('Received') - res.writeStatus("400 Bad Request").end('Missing room id'); + if (typeof roomId !== "string") { + console.error("Received"); + res.writeStatus("400 Bad Request").end("Missing room id"); return; } - res.upgrade( - {roomId}, - websocketKey, websocketProtocol, websocketExtensions, context, - ); + res.upgrade({ roomId }, websocketKey, websocketProtocol, websocketExtensions, context); }, open: (ws) => { - console.log('Admin socket connect for room: '+ws.roomId); + console.log("Admin socket connect for room: " + ws.roomId); ws.disconnecting = false; socketManager.handleAdminRoom(ws as ExAdminSocketInterface, ws.roomId as string); @@ -74,24 +71,34 @@ export class IoSocketController { const roomId = ws.roomId as string; //TODO refactor message type and data - const message: {event: string, message: {type: string, message: unknown, userUuid: string}} = + const message: { event: string; message: { type: string; message: unknown; userUuid: string } } = JSON.parse(new TextDecoder("utf-8").decode(new Uint8Array(arrayBuffer))); - if(message.event === 'user-message') { - const messageToEmit = (message.message as { message: string, type: string, userUuid: string }); - if(messageToEmit.type === 'banned'){ - socketManager.emitBan(messageToEmit.userUuid, messageToEmit.message, messageToEmit.type, ws.roomId as string); + if (message.event === "user-message") { + const messageToEmit = message.message as { message: string; type: string; userUuid: string }; + if (messageToEmit.type === "banned") { + socketManager.emitBan( + messageToEmit.userUuid, + messageToEmit.message, + messageToEmit.type, + ws.roomId as string + ); } - if(messageToEmit.type === 'ban') { - socketManager.emitSendUserMessage(messageToEmit.userUuid, messageToEmit.message, messageToEmit.type, ws.roomId as string); + if (messageToEmit.type === "ban") { + socketManager.emitSendUserMessage( + messageToEmit.userUuid, + messageToEmit.message, + messageToEmit.type, + ws.roomId as string + ); } } - }catch (err) { + } catch (err) { console.error(err); } }, close: (ws, code, message) => { - const Client = (ws as ExAdminSocketInterface); + const Client = ws as ExAdminSocketInterface; try { Client.disconnecting = true; socketManager.leaveAdminRoom(Client); @@ -99,12 +106,12 @@ export class IoSocketController { console.error('An error occurred on admin "disconnect"'); console.error(e); } - } - }) + }, + }); } ioConnection() { - this.app.ws('/room', { + this.app.ws("/room", { /* Options */ //compression: uWS.SHARED_COMPRESSOR, idleTimeout: SOCKET_IDLE_TIMER, @@ -114,7 +121,7 @@ export class IoSocketController { upgrade: (res, req, context) => { (async () => { /* Keep track of abortions */ - const upgradeAborted = {aborted: false}; + const upgradeAborted = { aborted: false }; res.onAborted(() => { /* We can simply signal that we were aborted */ @@ -123,15 +130,15 @@ export class IoSocketController { const url = req.getUrl(); const query = parse(req.getQuery()); - const websocketKey = req.getHeader('sec-websocket-key'); - const websocketProtocol = req.getHeader('sec-websocket-protocol'); - const websocketExtensions = req.getHeader('sec-websocket-extensions'); - const IPAddress = req.getHeader('x-forwarded-for'); + const websocketKey = req.getHeader("sec-websocket-key"); + const websocketProtocol = req.getHeader("sec-websocket-protocol"); + const websocketExtensions = req.getHeader("sec-websocket-extensions"); + const IPAddress = req.getHeader("x-forwarded-for"); const roomId = query.roomId; try { - if (typeof roomId !== 'string') { - throw new Error('Undefined room ID: '); + if (typeof roomId !== "string") { + throw new Error("Undefined room ID: "); } const token = query.token; @@ -143,62 +150,69 @@ export class IoSocketController { const right = Number(query.right); const name = query.name; - let companion: CompanionMessage|undefined = undefined; + let companion: CompanionMessage | undefined = undefined; - if (typeof query.companion === 'string') { + if (typeof query.companion === "string") { companion = new CompanionMessage(); companion.setName(query.companion); } - if (typeof name !== 'string') { - throw new Error('Expecting name'); + if (typeof name !== "string") { + throw new Error("Expecting name"); } - if (name === '') { - throw new Error('No empty name'); + if (name === "") { + throw new Error("No empty name"); } let characterLayers = query.characterLayers; if (characterLayers === null) { - throw new Error('Expecting skin'); + throw new Error("Expecting skin"); } - if (typeof characterLayers === 'string') { - characterLayers = [ characterLayers ]; + if (typeof characterLayers === "string") { + characterLayers = [characterLayers]; } const userUuid = await jwtTokenManager.getUserUuidFromToken(token, IPAddress, roomId); let memberTags: string[] = []; - let memberVisitCardUrl: string|null = null; + let memberVisitCardUrl: string | null = null; let memberMessages: unknown; let memberTextures: CharacterTexture[] = []; const room = await socketManager.getOrCreateRoom(roomId); if (ADMIN_API_URL) { try { - let userData : FetchMemberDataByUuidResponse = { + let userData: FetchMemberDataByUuidResponse = { uuid: v4(), tags: [], visitCardUrl: null, textures: [], messages: [], - anonymous: true + anonymous: true, }; try { userData = await adminApi.fetchMemberDataByUuid(userUuid, roomId); - }catch (err){ + } catch (err) { if (err?.response?.status == 404) { // If we get an HTTP 404, the token is invalid. Let's perform an anonymous login! - console.warn('Cannot find user with uuid "'+userUuid+'". Performing an anonymous login instead.'); - } else if(err?.response?.status == 403) { + console.warn( + 'Cannot find user with uuid "' + + userUuid + + '". Performing an anonymous login instead.' + ); + } else if (err?.response?.status == 403) { // If we get an HTTP 403, the world is full. We need to broadcast a special error to the client. // we finish immediately the upgrade then we will close the socket as soon as it starts opening. - return res.upgrade({ - rejected: true, - message: err?.response?.data.message, - status: err?.response?.status - }, websocketKey, - websocketProtocol, - websocketExtensions, - context); - }else{ + return res.upgrade( + { + rejected: true, + message: err?.response?.data.message, + status: err?.response?.status, + }, + websocketKey, + websocketProtocol, + websocketExtensions, + context + ); + } else { throw err; } } @@ -206,21 +220,30 @@ export class IoSocketController { memberTags = userData.tags; memberVisitCardUrl = userData.visitCardUrl; memberTextures = userData.textures; - if (!room.public && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && (userData.anonymous === true || !room.canAccess(memberTags))) { - throw new Error('Insufficient privileges to access this room') + if ( + !room.public && + room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && + (userData.anonymous === true || !room.canAccess(memberTags)) + ) { + throw new Error("Insufficient privileges to access this room"); } - if (!room.public && room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY && userData.anonymous === true) { - throw new Error('Use the login URL to connect') + if ( + !room.public && + room.policyType === GameRoomPolicyTypes.MEMBERS_ONLY_POLICY && + userData.anonymous === true + ) { + throw new Error("Use the login URL to connect"); } } catch (e) { - console.log('access not granted for user '+userUuid+' and room '+roomId); + console.log("access not granted for user " + userUuid + " and room " + roomId); console.error(e); - throw new Error('User cannot access this world') + throw new Error("User cannot access this world"); } } // Generate characterLayers objects from characterLayers string[] - const characterLayerObjs: CharacterLayer[] = SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures); + const characterLayerObjs: CharacterLayer[] = + SocketManager.mergeCharacterLayersAndCustomTextures(characterLayers, memberTextures); if (upgradeAborted.aborted) { console.log("Ouch! Client disconnected before we could upgrade it!"); @@ -229,7 +252,8 @@ export class IoSocketController { } /* This immediately calls open handler, you must not use res after this call */ - res.upgrade({ + res.upgrade( + { // Data passed here is accessible on the "websocket" socket object. url, token, @@ -246,22 +270,22 @@ export class IoSocketController { position: { x: x, y: y, - direction: 'down', - moving: false + direction: "down", + moving: false, } as PointInterface, viewport: { top, right, bottom, - left - } + left, + }, }, /* Spell these correctly */ websocketKey, websocketProtocol, websocketExtensions, - context); - + context + ); } catch (e) { /*if (e instanceof Error) { console.log(e.message); @@ -269,23 +293,26 @@ export class IoSocketController { } else { res.writeStatus("500 Internal Server Error").end('An error occurred'); }*/ - return res.upgrade({ - rejected: true, - message: e.message ? e.message : '500 Internal Server Error' - }, websocketKey, - websocketProtocol, - websocketExtensions, - context); + return res.upgrade( + { + rejected: true, + message: e.message ? e.message : "500 Internal Server Error", + }, + websocketKey, + websocketProtocol, + websocketExtensions, + context + ); } })(); }, /* Handlers */ open: (ws) => { - if(ws.rejected === true) { + if (ws.rejected === true) { //FIX ME to use status code - if(ws.message === 'World is full'){ + if (ws.message === "World is full") { socketManager.emitWorldFullMessage(ws); - }else{ + } else { socketManager.emitConnexionErrorMessage(ws, ws.message as string); } ws.close(); @@ -299,7 +326,7 @@ export class IoSocketController { //get data information and show messages if (client.messages && Array.isArray(client.messages)) { client.messages.forEach((c: unknown) => { - const messageToSend = c as { type: string, message: string }; + const messageToSend = c as { type: string; message: string }; const sendUserMessage = new SendUserMessage(); sendUserMessage.setType(messageToSend.type); @@ -323,33 +350,48 @@ export class IoSocketController { } else if (message.hasUsermovesmessage()) { socketManager.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage); } else if (message.hasSetplayerdetailsmessage()) { - socketManager.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage); + socketManager.handleSetPlayerDetails( + client, + message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage + ); } else if (message.hasSilentmessage()) { socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage); } else if (message.hasItemeventmessage()) { socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage); } else if (message.hasWebrtcsignaltoservermessage()) { - socketManager.emitVideo(client, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitVideo( + client, + message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage + ); } else if (message.hasWebrtcscreensharingsignaltoservermessage()) { - socketManager.emitScreenSharing(client, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitScreenSharing( + client, + message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage + ); } else if (message.hasPlayglobalmessage()) { socketManager.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage); - } else if (message.hasReportplayermessage()){ + } else if (message.hasReportplayermessage()) { socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage); - } else if (message.hasQueryjitsijwtmessage()){ - socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); - } else if (message.hasEmotepromptmessage()){ - socketManager.handleEmotePromptMessage(client, message.getEmotepromptmessage() as EmotePromptMessage); + } else if (message.hasQueryjitsijwtmessage()) { + socketManager.handleQueryJitsiJwtMessage( + client, + message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage + ); + } else if (message.hasEmotepromptmessage()) { + socketManager.handleEmotePromptMessage( + client, + message.getEmotepromptmessage() as EmotePromptMessage + ); } - /* Ok is false if backpressure was built up, wait for drain */ + /* Ok is false if backpressure was built up, wait for drain */ //let ok = ws.send(message, isBinary); }, drain: (ws) => { - console.log('WebSocket backpressure: ' + ws.getBufferedAmount()); + console.log("WebSocket backpressure: " + ws.getBufferedAmount()); }, close: (ws, code, message) => { - const Client = (ws as ExSocketInterface); + const Client = ws as ExSocketInterface; try { Client.disconnecting = true; //leave room @@ -358,13 +400,13 @@ export class IoSocketController { console.error('An error occurred on "disconnect"'); console.error(e); } - } - }) + }, + }); } //eslint-disable-next-line @typescript-eslint/no-explicit-any private initClient(ws: any): ExSocketInterface { - const client : ExSocketInterface = ws; + const client: ExSocketInterface = ws; client.userId = this.nextUserId; this.nextUserId++; client.userUuid = ws.userUuid; @@ -374,7 +416,7 @@ export class IoSocketController { client.batchTimeout = null; client.emitInBatch = (payload: SubMessage): void => { emitInBatch(client, payload); - } + }; client.disconnecting = false; client.messages = ws.messages; diff --git a/pusher/src/Controller/MapController.ts b/pusher/src/Controller/MapController.ts index 1ce04265..1df828d4 100644 --- a/pusher/src/Controller/MapController.ts +++ b/pusher/src/Controller/MapController.ts @@ -1,18 +1,15 @@ -import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js"; -import {BaseController} from "./BaseController"; -import {parse} from "query-string"; -import {adminApi} from "../Services/AdminApi"; +import { HttpRequest, HttpResponse, TemplatedApp } from "uWebSockets.js"; +import { BaseController } from "./BaseController"; +import { parse } from "query-string"; +import { adminApi } from "../Services/AdminApi"; - -export class MapController extends BaseController{ - - constructor(private App : TemplatedApp) { +export class MapController extends BaseController { + constructor(private App: TemplatedApp) { super(); this.App = App; this.getMapUrl(); } - // Returns a map mapping map name to file name of the map getMapUrl() { this.App.options("/map", (res: HttpResponse, req: HttpRequest) => { @@ -22,29 +19,28 @@ export class MapController extends BaseController{ }); this.App.get("/map", (res: HttpResponse, req: HttpRequest) => { - res.onAborted(() => { - console.warn('/map request was aborted'); - }) + console.warn("/map request was aborted"); + }); const query = parse(req.getQuery()); - if (typeof query.organizationSlug !== 'string') { - console.error('Expected organizationSlug parameter'); + if (typeof query.organizationSlug !== "string") { + console.error("Expected organizationSlug parameter"); res.writeStatus("400 Bad request"); this.addCorsHeaders(res); res.end("Expected organizationSlug parameter"); return; } - if (typeof query.worldSlug !== 'string') { - console.error('Expected worldSlug parameter'); + if (typeof query.worldSlug !== "string") { + console.error("Expected worldSlug parameter"); res.writeStatus("400 Bad request"); this.addCorsHeaders(res); res.end("Expected worldSlug parameter"); return; } - if (typeof query.roomSlug !== 'string' && query.roomSlug !== undefined) { - console.error('Expected only one roomSlug parameter'); + if (typeof query.roomSlug !== "string" && query.roomSlug !== undefined) { + console.error("Expected only one roomSlug parameter"); res.writeStatus("400 Bad request"); this.addCorsHeaders(res); res.end("Expected only one roomSlug parameter"); @@ -53,7 +49,11 @@ export class MapController extends BaseController{ (async () => { try { - const mapDetails = await adminApi.fetchMapDetails(query.organizationSlug as string, query.worldSlug as string, query.roomSlug as string|undefined); + const mapDetails = await adminApi.fetchMapDetails( + query.organizationSlug as string, + query.worldSlug as string, + query.roomSlug as string | undefined + ); res.writeStatus("200 OK"); this.addCorsHeaders(res); @@ -62,7 +62,6 @@ export class MapController extends BaseController{ this.errorToResponse(e, res); } })(); - }); } } diff --git a/pusher/src/Controller/PrometheusController.ts b/pusher/src/Controller/PrometheusController.ts index e854cf43..3ab3d33f 100644 --- a/pusher/src/Controller/PrometheusController.ts +++ b/pusher/src/Controller/PrometheusController.ts @@ -1,7 +1,7 @@ -import {App} from "../Server/sifrr.server"; -import {HttpRequest, HttpResponse} from "uWebSockets.js"; -const register = require('prom-client').register; -const collectDefaultMetrics = require('prom-client').collectDefaultMetrics; +import { App } from "../Server/sifrr.server"; +import { HttpRequest, HttpResponse } from "uWebSockets.js"; +const register = require("prom-client").register; +const collectDefaultMetrics = require("prom-client").collectDefaultMetrics; export class PrometheusController { constructor(private App: App) { @@ -14,7 +14,7 @@ export class PrometheusController { } private metrics(res: HttpResponse, req: HttpRequest): void { - res.writeHeader('Content-Type', register.contentType); + res.writeHeader("Content-Type", register.contentType); res.end(register.metrics()); } } diff --git a/pusher/src/Enum/EnvironmentVariable.ts b/pusher/src/Enum/EnvironmentVariable.ts index 5b3ec9c4..be974697 100644 --- a/pusher/src/Enum/EnvironmentVariable.ts +++ b/pusher/src/Enum/EnvironmentVariable.ts @@ -1,16 +1,16 @@ const SECRET_KEY = process.env.SECRET_KEY || "THECODINGMACHINE_SECRET_KEY"; const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64; const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; -const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false; -const API_URL = process.env.API_URL || ''; -const ADMIN_API_URL = process.env.ADMIN_API_URL || ''; -const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken'; -const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || '') || 600; +const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == "true" : false; +const API_URL = process.env.API_URL || ""; +const ADMIN_API_URL = process.env.ADMIN_API_URL || ""; +const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || "myapitoken"; +const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || "") || 600; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; -const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; -const JITSI_ISS = process.env.JITSI_ISS || ''; -const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ''; -const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || '8080') || 8080 +const JITSI_URL: string | undefined = process.env.JITSI_URL === "" ? undefined : process.env.JITSI_URL; +const JITSI_ISS = process.env.JITSI_ISS || ""; +const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ""; +const PUSHER_HTTP_PORT = parseInt(process.env.PUSHER_HTTP_PORT || "8080") || 8080; export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed export { @@ -26,5 +26,5 @@ export { JITSI_URL, JITSI_ISS, SECRET_JITSI_KEY, - PUSHER_HTTP_PORT -} + PUSHER_HTTP_PORT, +}; diff --git a/pusher/src/Model/Movable.ts b/pusher/src/Model/Movable.ts index 173db0ae..ca586b7c 100644 --- a/pusher/src/Model/Movable.ts +++ b/pusher/src/Model/Movable.ts @@ -1,8 +1,8 @@ -import {PositionInterface} from "_Model/PositionInterface"; +import { PositionInterface } from "_Model/PositionInterface"; /** * A physical object that can be placed into a Zone */ export interface Movable { - getPosition(): PositionInterface + getPosition(): PositionInterface; } diff --git a/pusher/src/Model/PositionDispatcher.ts b/pusher/src/Model/PositionDispatcher.ts index 1150394b..594328e3 100644 --- a/pusher/src/Model/PositionDispatcher.ts +++ b/pusher/src/Model/PositionDispatcher.ts @@ -8,9 +8,9 @@ * The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted * number of players around the current player. */ -import {Zone, ZoneEventListener} from "./Zone"; -import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; -import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; +import { Zone, ZoneEventListener } from "./Zone"; +import { ViewportInterface } from "_Model/Websocket/ViewportMessage"; +import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface"; //import Debug from "debug"; //const debug = Debug('positiondispatcher'); @@ -21,19 +21,22 @@ interface ZoneDescriptor { } export class PositionDispatcher { - // TODO: we need a way to clean the zones if noone is in the zone and noone listening (to free memory!) private zones: Zone[][] = []; - constructor(public readonly roomId: string, private zoneWidth: number, private zoneHeight: number, private socketListener: ZoneEventListener) { - } + constructor( + public readonly roomId: string, + private zoneWidth: number, + private zoneHeight: number, + private socketListener: ZoneEventListener + ) {} private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor { return { i: Math.floor(x / this.zoneWidth), j: Math.floor(y / this.zoneHeight), - } + }; } /** @@ -41,7 +44,7 @@ export class PositionDispatcher { */ public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void { if (viewport.left > viewport.right || viewport.top > viewport.bottom) { - console.warn('Invalid viewport received: ', viewport); + console.warn("Invalid viewport received: ", viewport); return; } @@ -57,8 +60,8 @@ export class PositionDispatcher { } } - const addedZones = [...newZones].filter(x => !oldZones.has(x)); - const removedZones = [...oldZones].filter(x => !newZones.has(x)); + const addedZones = [...newZones].filter((x) => !oldZones.has(x)); + const removedZones = [...oldZones].filter((x) => !newZones.has(x)); for (const zone of addedZones) { zone.startListening(socket); diff --git a/pusher/src/Model/PositionInterface.ts b/pusher/src/Model/PositionInterface.ts index d3b0dd47..65636759 100644 --- a/pusher/src/Model/PositionInterface.ts +++ b/pusher/src/Model/PositionInterface.ts @@ -1,4 +1,4 @@ export interface PositionInterface { - x: number, - y: number + x: number; + y: number; } diff --git a/pusher/src/Model/PusherRoom.ts b/pusher/src/Model/PusherRoom.ts index dcea5859..a49fce3e 100644 --- a/pusher/src/Model/PusherRoom.ts +++ b/pusher/src/Model/PusherRoom.ts @@ -1,9 +1,9 @@ -import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; -import {PositionDispatcher} from "./PositionDispatcher"; -import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; -import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier"; -import {arrayIntersect} from "../Services/ArrayHelper"; -import {ZoneEventListener} from "_Model/Zone"; +import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface"; +import { PositionDispatcher } from "./PositionDispatcher"; +import { ViewportInterface } from "_Model/Websocket/ViewportMessage"; +import { extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous } from "./RoomIdentifier"; +import { arrayIntersect } from "../Services/ArrayHelper"; +import { ZoneEventListener } from "_Model/Zone"; export enum GameRoomPolicyTypes { ANONYMUS_POLICY = 1, @@ -17,13 +17,11 @@ export class PusherRoom { public tags: string[]; public policyType: GameRoomPolicyTypes; public readonly roomSlug: string; - public readonly worldSlug: string = ''; - public readonly organizationSlug: string = ''; + public readonly worldSlug: string = ""; + public readonly organizationSlug: string = ""; private versionNumber: number = 1; - constructor(public readonly roomId: string, - private socketListener: ZoneEventListener) - { + constructor(public readonly roomId: string, private socketListener: ZoneEventListener) { this.public = isRoomAnonymous(roomId); this.tags = []; this.policyType = GameRoomPolicyTypes.ANONYMUS_POLICY; @@ -31,7 +29,7 @@ export class PusherRoom { if (this.public) { this.roomSlug = extractRoomSlugPublicRoomId(this.roomId); } else { - const {organizationSlug, worldSlug, roomSlug} = extractDataFromPrivateRoomId(this.roomId); + const { organizationSlug, worldSlug, roomSlug } = extractDataFromPrivateRoomId(this.roomId); this.roomSlug = roomSlug; this.organizationSlug = organizationSlug; this.worldSlug = worldSlug; @@ -41,11 +39,11 @@ export class PusherRoom { this.positionNotifier = new PositionDispatcher(this.roomId, 320, 320, this.socketListener); } - public setViewport(socket : ExSocketInterface, viewport: ViewportInterface): void { + public setViewport(socket: ExSocketInterface, viewport: ViewportInterface): void { this.positionNotifier.setViewport(socket, viewport); } - public leave(socket : ExSocketInterface){ + public leave(socket: ExSocketInterface) { this.positionNotifier.removeViewport(socket); } diff --git a/pusher/src/Model/RoomIdentifier.ts b/pusher/src/Model/RoomIdentifier.ts index 3ac62bca..d1de8800 100644 --- a/pusher/src/Model/RoomIdentifier.ts +++ b/pusher/src/Model/RoomIdentifier.ts @@ -1,30 +1,30 @@ //helper functions to parse room IDs export const isRoomAnonymous = (roomID: string): boolean => { - if (roomID.startsWith('_/')) { + if (roomID.startsWith("_/")) { return true; - } else if(roomID.startsWith('@/')) { + } else if (roomID.startsWith("@/")) { return false; } else { - throw new Error('Incorrect room ID: '+roomID); + throw new Error("Incorrect room ID: " + roomID); } -} +}; export const extractRoomSlugPublicRoomId = (roomId: string): string => { - const idParts = roomId.split('/'); - if (idParts.length < 3) throw new Error('Incorrect roomId: '+roomId); - return idParts.slice(2).join('/'); -} + const idParts = roomId.split("/"); + if (idParts.length < 3) throw new Error("Incorrect roomId: " + roomId); + return idParts.slice(2).join("/"); +}; export interface extractDataFromPrivateRoomIdResponse { organizationSlug: string; worldSlug: string; roomSlug: string; } export const extractDataFromPrivateRoomId = (roomId: string): extractDataFromPrivateRoomIdResponse => { - const idParts = roomId.split('/'); - if (idParts.length < 4) throw new Error('Incorrect roomId: '+roomId); + const idParts = roomId.split("/"); + if (idParts.length < 4) throw new Error("Incorrect roomId: " + roomId); const organizationSlug = idParts[1]; const worldSlug = idParts[2]; const roomSlug = idParts[3]; - return {organizationSlug, worldSlug, roomSlug} -} \ No newline at end of file + return { organizationSlug, worldSlug, roomSlug }; +}; diff --git a/pusher/src/Model/Websocket/ExAdminSocketInterface.ts b/pusher/src/Model/Websocket/ExAdminSocketInterface.ts index 1e03db6c..7599c82c 100644 --- a/pusher/src/Model/Websocket/ExAdminSocketInterface.ts +++ b/pusher/src/Model/Websocket/ExAdminSocketInterface.ts @@ -1,21 +1,22 @@ -import {PointInterface} from "./PointInterface"; -import {Identificable} from "./Identificable"; -import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; +import { PointInterface } from "./PointInterface"; +import { Identificable } from "./Identificable"; +import { ViewportInterface } from "_Model/Websocket/ViewportMessage"; import { AdminPusherToBackMessage, BatchMessage, - PusherToBackMessage, ServerToAdminClientMessage, + PusherToBackMessage, + ServerToAdminClientMessage, ServerToClientMessage, - SubMessage + SubMessage, } from "../../Messages/generated/messages_pb"; -import {WebSocket} from "uWebSockets.js" -import {CharacterTexture} from "../../Services/AdminApi"; -import {ClientDuplexStream} from "grpc"; -import {Zone} from "_Model/Zone"; +import { WebSocket } from "uWebSockets.js"; +import { CharacterTexture } from "../../Services/AdminApi"; +import { ClientDuplexStream } from "grpc"; +import { Zone } from "_Model/Zone"; export type AdminConnection = ClientDuplexStream; export interface ExAdminSocketInterface extends WebSocket { - adminConnection: AdminConnection, - disconnecting: boolean, + adminConnection: AdminConnection; + disconnecting: boolean; } diff --git a/pusher/src/Model/Websocket/ExSocketInterface.ts b/pusher/src/Model/Websocket/ExSocketInterface.ts index 98759142..9a92a0e7 100644 --- a/pusher/src/Model/Websocket/ExSocketInterface.ts +++ b/pusher/src/Model/Websocket/ExSocketInterface.ts @@ -1,23 +1,23 @@ -import {PointInterface} from "./PointInterface"; -import {Identificable} from "./Identificable"; -import {ViewportInterface} from "_Model/Websocket/ViewportMessage"; +import { PointInterface } from "./PointInterface"; +import { Identificable } from "./Identificable"; +import { ViewportInterface } from "_Model/Websocket/ViewportMessage"; import { BatchMessage, CompanionMessage, PusherToBackMessage, ServerToClientMessage, - SubMessage + SubMessage, } from "../../Messages/generated/messages_pb"; -import {WebSocket} from "uWebSockets.js" -import {CharacterTexture} from "../../Services/AdminApi"; -import {ClientDuplexStream} from "grpc"; -import {Zone} from "_Model/Zone"; +import { WebSocket } from "uWebSockets.js"; +import { CharacterTexture } from "../../Services/AdminApi"; +import { ClientDuplexStream } from "grpc"; +import { Zone } from "_Model/Zone"; export type BackConnection = ClientDuplexStream; export interface CharacterLayer { - name: string, - url: string|undefined + name: string; + url: string | undefined; } export interface ExSocketInterface extends WebSocket, Identificable { @@ -36,12 +36,12 @@ export interface ExSocketInterface extends WebSocket, Identificable { */ emitInBatch: (payload: SubMessage) => void; batchedMessages: BatchMessage; - batchTimeout: NodeJS.Timeout|null; - disconnecting: boolean, - messages: unknown, - tags: string[], - visitCardUrl: string|null, - textures: CharacterTexture[], - backConnection: BackConnection, + batchTimeout: NodeJS.Timeout | null; + disconnecting: boolean; + messages: unknown; + tags: string[]; + visitCardUrl: string | null; + textures: CharacterTexture[]; + backConnection: BackConnection; listenedZones: Set; } diff --git a/pusher/src/Model/Websocket/ItemEventMessage.ts b/pusher/src/Model/Websocket/ItemEventMessage.ts index b1f9203e..1bb7f615 100644 --- a/pusher/src/Model/Websocket/ItemEventMessage.ts +++ b/pusher/src/Model/Websocket/ItemEventMessage.ts @@ -1,10 +1,11 @@ import * as tg from "generic-type-guard"; -export const isItemEventMessageInterface = - new tg.IsInterface().withProperties({ +export const isItemEventMessageInterface = new tg.IsInterface() + .withProperties({ itemId: tg.isNumber, event: tg.isString, state: tg.isUnknown, parameters: tg.isUnknown, - }).get(); + }) + .get(); export type ItemEventMessageInterface = tg.GuardedType; diff --git a/pusher/src/Model/Websocket/Point.ts b/pusher/src/Model/Websocket/Point.ts index c66720ba..19b57d2e 100644 --- a/pusher/src/Model/Websocket/Point.ts +++ b/pusher/src/Model/Websocket/Point.ts @@ -1,6 +1,10 @@ -import {PointInterface} from "./PointInterface"; +import { PointInterface } from "./PointInterface"; -export class Point implements PointInterface{ - constructor(public x : number, public y : number, public direction : string = "none", public moving : boolean = false) { - } +export class Point implements PointInterface { + constructor( + public x: number, + public y: number, + public direction: string = "none", + public moving: boolean = false + ) {} } diff --git a/pusher/src/Model/Websocket/PointInterface.ts b/pusher/src/Model/Websocket/PointInterface.ts index afb07a23..d7c7826e 100644 --- a/pusher/src/Model/Websocket/PointInterface.ts +++ b/pusher/src/Model/Websocket/PointInterface.ts @@ -7,11 +7,12 @@ import * as tg from "generic-type-guard"; readonly moving: boolean; }*/ -export const isPointInterface = - new tg.IsInterface().withProperties({ +export const isPointInterface = new tg.IsInterface() + .withProperties({ x: tg.isNumber, y: tg.isNumber, direction: tg.isString, - moving: tg.isBoolean - }).get(); + moving: tg.isBoolean, + }) + .get(); export type PointInterface = tg.GuardedType; diff --git a/pusher/src/Model/Websocket/ProtobufUtils.ts b/pusher/src/Model/Websocket/ProtobufUtils.ts index 89a90acc..bd9cb9c2 100644 --- a/pusher/src/Model/Websocket/ProtobufUtils.ts +++ b/pusher/src/Model/Websocket/ProtobufUtils.ts @@ -1,34 +1,33 @@ -import {PointInterface} from "./PointInterface"; +import { PointInterface } from "./PointInterface"; import { CharacterLayerMessage, ItemEventMessage, PointMessage, - PositionMessage + PositionMessage, } from "../../Messages/generated/messages_pb"; -import {CharacterLayer, ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; +import { CharacterLayer, ExSocketInterface } from "_Model/Websocket/ExSocketInterface"; import Direction = PositionMessage.Direction; -import {ItemEventMessageInterface} from "_Model/Websocket/ItemEventMessage"; -import {PositionInterface} from "_Model/PositionInterface"; +import { ItemEventMessageInterface } from "_Model/Websocket/ItemEventMessage"; +import { PositionInterface } from "_Model/PositionInterface"; export class ProtobufUtils { - public static toPositionMessage(point: PointInterface): PositionMessage { let direction: Direction; switch (point.direction) { - case 'up': + case "up": direction = Direction.UP; break; - case 'down': + case "down": direction = Direction.DOWN; break; - case 'left': + case "left": direction = Direction.LEFT; break; - case 'right': + case "right": direction = Direction.RIGHT; break; default: - throw new Error('unexpected direction'); + throw new Error("unexpected direction"); } const position = new PositionMessage(); @@ -44,16 +43,16 @@ export class ProtobufUtils { let direction: string; switch (position.getDirection()) { case Direction.UP: - direction = 'up'; + direction = "up"; break; case Direction.DOWN: - direction = 'down'; + direction = "down"; break; case Direction.LEFT: - direction = 'left'; + direction = "left"; break; case Direction.RIGHT: - direction = 'right'; + direction = "right"; break; default: throw new Error("Unexpected direction"); @@ -82,7 +81,7 @@ export class ProtobufUtils { event: itemEventMessage.getEvent(), parameters: JSON.parse(itemEventMessage.getParametersjson()), state: JSON.parse(itemEventMessage.getStatejson()), - } + }; } public static toItemEventProtobuf(itemEvent: ItemEventMessageInterface): ItemEventMessage { @@ -96,7 +95,7 @@ export class ProtobufUtils { } public static toCharacterLayerMessages(characterLayers: CharacterLayer[]): CharacterLayerMessage[] { - return characterLayers.map(function(characterLayer): CharacterLayerMessage { + return characterLayers.map(function (characterLayer): CharacterLayerMessage { const message = new CharacterLayerMessage(); message.setName(characterLayer.name); if (characterLayer.url) { diff --git a/pusher/src/Model/Websocket/ViewportMessage.ts b/pusher/src/Model/Websocket/ViewportMessage.ts index 62e2fc81..ea71ad68 100644 --- a/pusher/src/Model/Websocket/ViewportMessage.ts +++ b/pusher/src/Model/Websocket/ViewportMessage.ts @@ -1,10 +1,11 @@ import * as tg from "generic-type-guard"; -export const isViewport = - new tg.IsInterface().withProperties({ +export const isViewport = new tg.IsInterface() + .withProperties({ left: tg.isNumber, top: tg.isNumber, right: tg.isNumber, bottom: tg.isNumber, - }).get(); + }) + .get(); export type ViewportInterface = tg.GuardedType; diff --git a/pusher/src/Model/Zone.ts b/pusher/src/Model/Zone.ts index 318a119b..8eeeb3ef 100644 --- a/pusher/src/Model/Zone.ts +++ b/pusher/src/Model/Zone.ts @@ -1,16 +1,23 @@ -import {ExSocketInterface} from "./Websocket/ExSocketInterface"; -import {apiClientRepository} from "../Services/ApiClientRepository"; +import { ExSocketInterface } from "./Websocket/ExSocketInterface"; +import { apiClientRepository } from "../Services/ApiClientRepository"; import { BatchToPusherMessage, - CharacterLayerMessage, GroupLeftZoneMessage, GroupUpdateMessage, GroupUpdateZoneMessage, - PointMessage, PositionMessage, UserJoinedMessage, - UserJoinedZoneMessage, UserLeftZoneMessage, UserMovedMessage, + CharacterLayerMessage, + GroupLeftZoneMessage, + GroupUpdateMessage, + GroupUpdateZoneMessage, + PointMessage, + PositionMessage, + UserJoinedMessage, + UserJoinedZoneMessage, + UserLeftZoneMessage, + UserMovedMessage, ZoneMessage, EmoteEventMessage, - CompanionMessage + CompanionMessage, } from "../Messages/generated/messages_pb"; -import {ClientReadableStream} from "grpc"; -import {PositionDispatcher} from "_Model/PositionDispatcher"; +import { ClientReadableStream } from "grpc"; +import { PositionDispatcher } from "_Model/PositionDispatcher"; import Debug from "debug"; const debug = Debug("zone"); @@ -30,24 +37,38 @@ export type MovesCallback = (thing: Movable, position: PositionInterface, listen export type LeavesCallback = (thing: Movable, listener: User) => void;*/ export class UserDescriptor { - private constructor(public readonly userId: number, private name: string, private characterLayers: CharacterLayerMessage[], private position: PositionMessage, private visitCardUrl: string | null, private companion?: CompanionMessage) { + private constructor( + public readonly userId: number, + private name: string, + private characterLayers: CharacterLayerMessage[], + private position: PositionMessage, + private visitCardUrl: string | null, + private companion?: CompanionMessage + ) { if (!Number.isInteger(this.userId)) { - throw new Error('UserDescriptor.userId is not an integer: '+this.userId); + throw new Error("UserDescriptor.userId is not an integer: " + this.userId); } } public static createFromUserJoinedZoneMessage(message: UserJoinedZoneMessage): UserDescriptor { const position = message.getPosition(); if (position === undefined) { - throw new Error('Missing position'); + throw new Error("Missing position"); } - return new UserDescriptor(message.getUserid(), message.getName(), message.getCharacterlayersList(), position, message.getVisitcardurl(), message.getCompanion()); + return new UserDescriptor( + message.getUserid(), + message.getName(), + message.getCharacterlayersList(), + position, + message.getVisitcardurl(), + message.getCompanion() + ); } public update(userMovedMessage: UserMovedMessage) { const position = userMovedMessage.getPosition(); if (position === undefined) { - throw new Error('Missing position'); + throw new Error("Missing position"); } this.position = position; } @@ -78,13 +99,12 @@ export class UserDescriptor { } export class GroupDescriptor { - private constructor(public readonly groupId: number, private groupSize: number, private position: PointMessage) { - } + private constructor(public readonly groupId: number, private groupSize: number, private position: PointMessage) {} public static createFromGroupUpdateZoneMessage(message: GroupUpdateZoneMessage): GroupDescriptor { const position = message.getPosition(); if (position === undefined) { - throw new Error('Missing position'); + throw new Error("Missing position"); } return new GroupDescriptor(message.getGroupid(), message.getGroupsize(), position); } @@ -97,7 +117,7 @@ export class GroupDescriptor { public toGroupUpdateMessage(): GroupUpdateMessage { const groupUpdateMessage = new GroupUpdateMessage(); if (!Number.isInteger(this.groupId)) { - throw new Error('GroupDescriptor.groupId is not an integer: '+this.groupId); + throw new Error("GroupDescriptor.groupId is not an integer: " + this.groupId); } groupUpdateMessage.setGroupid(this.groupId); groupUpdateMessage.setGroupsize(this.groupSize); @@ -108,8 +128,8 @@ export class GroupDescriptor { } interface ZoneDescriptor { - x: number, - y: number + x: number; + y: number; } export class Zone { @@ -120,21 +140,26 @@ export class Zone { private backConnection!: ClientReadableStream; private isClosing: boolean = false; - constructor(private positionDispatcher: PositionDispatcher, private socketListener: ZoneEventListener, public readonly x: number, public readonly y: number, private onBackFailure: (e: Error|null, zone: Zone) => void) { - } + constructor( + private positionDispatcher: PositionDispatcher, + private socketListener: ZoneEventListener, + public readonly x: number, + public readonly y: number, + private onBackFailure: (e: Error | null, zone: Zone) => void + ) {} /** * Creates a connection to the back server to track the users. */ public async init(): Promise { - debug('Opening connection to zone %d, %d on back server', this.x, this.y); + debug("Opening connection to zone %d, %d on back server", this.x, this.y); const apiClient = await apiClientRepository.getClient(this.positionDispatcher.roomId); const zoneMessage = new ZoneMessage(); zoneMessage.setRoomid(this.positionDispatcher.roomId); zoneMessage.setX(this.x); zoneMessage.setY(this.y); this.backConnection = apiClient.listenZone(zoneMessage); - this.backConnection.on('data', (batch: BatchToPusherMessage) => { + this.backConnection.on("data", (batch: BatchToPusherMessage) => { for (const message of batch.getPayloadList()) { if (message.hasUserjoinedzonemessage()) { const userJoinedZoneMessage = message.getUserjoinedzonemessage() as UserJoinedZoneMessage; @@ -179,33 +204,32 @@ export class Zone { const userDescriptor = this.users.get(userId); if (userDescriptor === undefined) { - console.error('Unexpected move message received for user "'+userId+'"'); + console.error('Unexpected move message received for user "' + userId + '"'); return; } userDescriptor.update(userMovedMessage); this.notifyUserMove(userDescriptor); - } else if(message.hasEmoteeventmessage()) { + } else if (message.hasEmoteeventmessage()) { const emoteEventMessage = message.getEmoteeventmessage() as EmoteEventMessage; this.notifyEmote(emoteEventMessage); } else { - throw new Error('Unexpected message'); + throw new Error("Unexpected message"); } - } }); - this.backConnection.on('error', (e) => { + this.backConnection.on("error", (e) => { if (!this.isClosing) { - debug('Error on back connection') + debug("Error on back connection"); this.close(); this.onBackFailure(e, this); } }); - this.backConnection.on('close', () => { + this.backConnection.on("close", () => { if (!this.isClosing) { - debug('Close on back connection') + debug("Close on back connection"); this.close(); this.onBackFailure(null, this); } @@ -213,7 +237,7 @@ export class Zone { } public close(): void { - debug('Closing connection to zone %d, %d on back server', this.x, this.y); + debug("Closing connection to zone %d, %d on back server", this.x, this.y); this.isClosing = true; this.backConnection.cancel(); } @@ -225,7 +249,7 @@ export class Zone { /** * Notify listeners of this zone that this user entered */ - private notifyUserEnter(user: UserDescriptor, oldZone: ZoneDescriptor|undefined) { + private notifyUserEnter(user: UserDescriptor, oldZone: ZoneDescriptor | undefined) { for (const listener of this.listeners) { if (listener.userId === user.userId) { continue; @@ -241,7 +265,7 @@ export class Zone { /** * Notify listeners of this zone that this group entered */ - private notifyGroupEnter(group: GroupDescriptor, oldZone: ZoneDescriptor|undefined) { + private notifyGroupEnter(group: GroupDescriptor, oldZone: ZoneDescriptor | undefined) { for (const listener of this.listeners) { if (oldZone === undefined || !this.isListeningZone(listener, oldZone.x, oldZone.y)) { this.socketListener.onGroupEnters(group, listener); @@ -254,7 +278,7 @@ export class Zone { /** * Notify listeners of this zone that this user left */ - private notifyUserLeft(userId: number, newZone: ZoneDescriptor|undefined) { + private notifyUserLeft(userId: number, newZone: ZoneDescriptor | undefined) { for (const listener of this.listeners) { if (listener.userId === userId) { continue; @@ -279,7 +303,7 @@ export class Zone { /** * Notify listeners of this zone that this group left */ - private notifyGroupLeft(groupId: number, newZone: ZoneDescriptor|undefined) { + private notifyGroupLeft(groupId: number, newZone: ZoneDescriptor | undefined) { for (const listener of this.listeners) { if (listener.groupId === groupId) { continue; diff --git a/pusher/src/Server/server/app.ts b/pusher/src/Server/server/app.ts index 3b98a9b3..4c422d5c 100644 --- a/pusher/src/Server/server/app.ts +++ b/pusher/src/Server/server/app.ts @@ -1,13 +1,13 @@ -import { App as _App, AppOptions } from 'uWebSockets.js'; -import BaseApp from './baseapp'; -import { extend } from './utils'; -import { UwsApp } from './types'; +import { App as _App, AppOptions } from "uWebSockets.js"; +import BaseApp from "./baseapp"; +import { extend } from "./utils"; +import { UwsApp } from "./types"; class App extends (_App) { - constructor(options: AppOptions = {}) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions = {}) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default App; diff --git a/pusher/src/Server/server/baseapp.ts b/pusher/src/Server/server/baseapp.ts index accd8a99..6d973ac7 100644 --- a/pusher/src/Server/server/baseapp.ts +++ b/pusher/src/Server/server/baseapp.ts @@ -1,116 +1,109 @@ -import { Readable } from 'stream'; -import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; +import { Readable } from "stream"; +import { us_listen_socket_close, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; -import formData from './formdata'; -import { stob } from './utils'; -import { Handler } from './types'; -import {join} from "path"; +import formData from "./formdata"; +import { stob } from "./utils"; +import { Handler } from "./types"; +import { join } from "path"; -const contTypes = ['application/x-www-form-urlencoded', 'multipart/form-data']; +const contTypes = ["application/x-www-form-urlencoded", "multipart/form-data"]; const noOp = () => true; const handleBody = (res: HttpResponse, req: HttpRequest) => { - const contType = req.getHeader('content-type'); + const contType = req.getHeader("content-type"); - res.bodyStream = function() { - const stream = new Readable(); - stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method + res.bodyStream = function () { + const stream = new Readable(); + stream._read = noOp; // eslint-disable-line @typescript-eslint/unbound-method - this.onData((ab: ArrayBuffer, isLast: boolean) => { - // uint and then slicing is bit faster than slice and then uint - stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any - if (isLast) { - stream.push(null); - } - }); + this.onData((ab: ArrayBuffer, isLast: boolean) => { + // uint and then slicing is bit faster than slice and then uint + stream.push(new Uint8Array(ab.slice((ab as any).byteOffset, ab.byteLength))); // eslint-disable-line @typescript-eslint/no-explicit-any + if (isLast) { + stream.push(null); + } + }); - return stream; - }; + return stream; + }; - res.body = () => stob(res.bodyStream()); + res.body = () => stob(res.bodyStream()); - if (contType.includes('application/json')) - res.json = async () => JSON.parse(await res.body()); - if (contTypes.map(t => contType.includes(t)).includes(true)) - res.formData = formData.bind(res, contType); + if (contType.includes("application/json")) res.json = async () => JSON.parse(await res.body()); + if (contTypes.map((t) => contType.includes(t)).includes(true)) res.formData = formData.bind(res, contType); }; class BaseApp { - _sockets = new Map(); - ws!: TemplatedApp['ws']; - get!: TemplatedApp['get']; - _post!: TemplatedApp['post']; - _put!: TemplatedApp['put']; - _patch!: TemplatedApp['patch']; - _listen!: TemplatedApp['listen']; + _sockets = new Map(); + ws!: TemplatedApp["ws"]; + get!: TemplatedApp["get"]; + _post!: TemplatedApp["post"]; + _put!: TemplatedApp["put"]; + _patch!: TemplatedApp["patch"]; + _listen!: TemplatedApp["listen"]; - post(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._post(pattern, (res, req) => { - handleBody(res, req); - handler(res, req); - }); - return this; - } + post(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._post(pattern, (res, req) => { + handleBody(res, req); + handler(res, req); + }); + return this; + } - put(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._put(pattern, (res, req) => { - handleBody(res, req); + put(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._put(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - patch(pattern: string, handler: Handler) { - if (typeof handler !== 'function') - throw Error(`handler should be a function, given ${typeof handler}.`); - this._patch(pattern, (res, req) => { - handleBody(res, req); + patch(pattern: string, handler: Handler) { + if (typeof handler !== "function") throw Error(`handler should be a function, given ${typeof handler}.`); + this._patch(pattern, (res, req) => { + handleBody(res, req); - handler(res, req); - }); - return this; - } + handler(res, req); + }); + return this; + } - listen(h: string | number, p: Function | number = noOp, cb?: Function) { - if (typeof p === 'number' && typeof h === 'string') { - this._listen(h, p, socket => { - this._sockets.set(p, socket); - if (cb === undefined) { - throw new Error('cb undefined'); + listen(h: string | number, p: Function | number = noOp, cb?: Function) { + if (typeof p === "number" && typeof h === "string") { + this._listen(h, p, (socket) => { + this._sockets.set(p, socket); + if (cb === undefined) { + throw new Error("cb undefined"); + } + cb(socket); + }); + } else if (typeof h === "number" && typeof p === "function") { + this._listen(h, (socket) => { + this._sockets.set(h, socket); + p(socket); + }); + } else { + throw Error("Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)"); } - cb(socket); - }); - } else if (typeof h === 'number' && typeof p === 'function') { - this._listen(h, socket => { - this._sockets.set(h, socket); - p(socket); - }); - } else { - throw Error( - 'Argument types: (host: string, port: number, cb?: Function) | (port: number, cb?: Function)' - ); + + return this; } - return this; - } - - close(port: null | number = null) { - if (port) { - this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); - this._sockets.delete(port); - } else { - this._sockets.forEach(app => { - us_listen_socket_close(app); - }); - this._sockets.clear(); + close(port: null | number = null) { + if (port) { + this._sockets.has(port) && us_listen_socket_close(this._sockets.get(port)); + this._sockets.delete(port); + } else { + this._sockets.forEach((app) => { + us_listen_socket_close(app); + }); + this._sockets.clear(); + } + return this; } - return this; - } } export default BaseApp; diff --git a/pusher/src/Server/server/formdata.ts b/pusher/src/Server/server/formdata.ts index 9dd08440..66e51db4 100644 --- a/pusher/src/Server/server/formdata.ts +++ b/pusher/src/Server/server/formdata.ts @@ -1,100 +1,99 @@ -import { createWriteStream } from 'fs'; -import { join, dirname } from 'path'; -import Busboy from 'busboy'; -import mkdirp from 'mkdirp'; +import { createWriteStream } from "fs"; +import { join, dirname } from "path"; +import Busboy from "busboy"; +import mkdirp from "mkdirp"; function formData( - contType: string, - options: busboy.BusboyConfig & { - abortOnLimit?: boolean; - tmpDir?: string; - onFile?: ( - fieldname: string, - file: NodeJS.ReadableStream, - filename: string, - encoding: string, - mimetype: string - ) => string; - onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any - filename?: (oldName: string) => string; - } = {} + contType: string, + options: busboy.BusboyConfig & { + abortOnLimit?: boolean; + tmpDir?: string; + onFile?: ( + fieldname: string, + file: NodeJS.ReadableStream, + filename: string, + encoding: string, + mimetype: string + ) => string; + onField?: (fieldname: string, value: any) => void; // eslint-disable-line @typescript-eslint/no-explicit-any + filename?: (oldName: string) => string; + } = {} ) { - console.log('Enter form data'); - options.headers = { - 'content-type': contType - }; + console.log("Enter form data"); + options.headers = { + "content-type": contType, + }; - return new Promise((resolve, reject) => { - const busb = new Busboy(options); - const ret = {}; + return new Promise((resolve, reject) => { + const busb = new Busboy(options); + const ret = {}; - this.bodyStream().pipe(busb); + this.bodyStream().pipe(busb); - busb.on('limit', () => { - if (options.abortOnLimit) { - reject(Error('limit')); - } + busb.on("limit", () => { + if (options.abortOnLimit) { + reject(Error("limit")); + } + }); + + busb.on("file", function (fieldname, file, filename, encoding, mimetype) { + const value: { filePath: string | undefined; filename: string; encoding: string; mimetype: string } = { + filename, + encoding, + mimetype, + filePath: undefined, + }; + + if (typeof options.tmpDir === "string") { + if (typeof options.filename === "function") filename = options.filename(filename); + const fileToSave = join(options.tmpDir, filename); + mkdirp(dirname(fileToSave)); + + file.pipe(createWriteStream(fileToSave)); + value.filePath = fileToSave; + } + if (typeof options.onFile === "function") { + value.filePath = options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath; + } + + setRetValue(ret, fieldname, value); + }); + + busb.on("field", function (fieldname, value) { + if (typeof options.onField === "function") options.onField(fieldname, value); + + setRetValue(ret, fieldname, value); + }); + + busb.on("finish", function () { + resolve(ret); + }); + + busb.on("error", reject); }); - - busb.on('file', function(fieldname, file, filename, encoding, mimetype) { - const value: { filePath: string|undefined, filename: string, encoding:string, mimetype: string } = { - filename, - encoding, - mimetype, - filePath: undefined - }; - - if (typeof options.tmpDir === 'string') { - if (typeof options.filename === 'function') filename = options.filename(filename); - const fileToSave = join(options.tmpDir, filename); - mkdirp(dirname(fileToSave)); - - file.pipe(createWriteStream(fileToSave)); - value.filePath = fileToSave; - } - if (typeof options.onFile === 'function') { - value.filePath = - options.onFile(fieldname, file, filename, encoding, mimetype) || value.filePath; - } - - setRetValue(ret, fieldname, value); - }); - - busb.on('field', function(fieldname, value) { - if (typeof options.onField === 'function') options.onField(fieldname, value); - - setRetValue(ret, fieldname, value); - }); - - busb.on('finish', function() { - resolve(ret); - }); - - busb.on('error', reject); - }); } function setRetValue( - ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any - fieldname: string, - value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any + ret: { [x: string]: any }, // eslint-disable-line @typescript-eslint/no-explicit-any + fieldname: string, + value: { filename: string; encoding: string; mimetype: string; filePath?: string } | any // eslint-disable-line @typescript-eslint/no-explicit-any ) { - if (fieldname.endsWith('[]')) { - fieldname = fieldname.slice(0, fieldname.length - 2); - if (Array.isArray(ret[fieldname])) { - ret[fieldname].push(value); + if (fieldname.endsWith("[]")) { + fieldname = fieldname.slice(0, fieldname.length - 2); + if (Array.isArray(ret[fieldname])) { + ret[fieldname].push(value); + } else { + ret[fieldname] = [value]; + } } else { - ret[fieldname] = [value]; + if (Array.isArray(ret[fieldname])) { + ret[fieldname].push(value); + } else if (ret[fieldname]) { + ret[fieldname] = [ret[fieldname], value]; + } else { + ret[fieldname] = value; + } } - } else { - if (Array.isArray(ret[fieldname])) { - ret[fieldname].push(value); - } else if (ret[fieldname]) { - ret[fieldname] = [ret[fieldname], value]; - } else { - ret[fieldname] = value; - } - } } export default formData; diff --git a/pusher/src/Server/server/sslapp.ts b/pusher/src/Server/server/sslapp.ts index 46ae89a5..80df0e4a 100644 --- a/pusher/src/Server/server/sslapp.ts +++ b/pusher/src/Server/server/sslapp.ts @@ -1,13 +1,13 @@ -import { SSLApp as _SSLApp, AppOptions } from 'uWebSockets.js'; -import BaseApp from './baseapp'; -import { extend } from './utils'; -import { UwsApp } from './types'; +import { SSLApp as _SSLApp, AppOptions } from "uWebSockets.js"; +import BaseApp from "./baseapp"; +import { extend } from "./utils"; +import { UwsApp } from "./types"; class SSLApp extends (_SSLApp) { - constructor(options: AppOptions) { - super(options); // eslint-disable-line constructor-super - extend(this, new BaseApp()); - } + constructor(options: AppOptions) { + super(options); // eslint-disable-line constructor-super + extend(this, new BaseApp()); + } } export default SSLApp; diff --git a/pusher/src/Server/server/types.ts b/pusher/src/Server/server/types.ts index 3d0f48c7..afc21d17 100644 --- a/pusher/src/Server/server/types.ts +++ b/pusher/src/Server/server/types.ts @@ -1,9 +1,9 @@ -import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from 'uWebSockets.js'; +import { AppOptions, TemplatedApp, HttpResponse, HttpRequest } from "uWebSockets.js"; export type UwsApp = { - (options: AppOptions): TemplatedApp; - new (options: AppOptions): TemplatedApp; - prototype: TemplatedApp; + (options: AppOptions): TemplatedApp; + new (options: AppOptions): TemplatedApp; + prototype: TemplatedApp; }; export type Handler = (res: HttpResponse, req: HttpRequest) => void; diff --git a/pusher/src/Server/server/utils.ts b/pusher/src/Server/server/utils.ts index 80ea3938..dc813064 100644 --- a/pusher/src/Server/server/utils.ts +++ b/pusher/src/Server/server/utils.ts @@ -1,37 +1,36 @@ -import { ReadStream } from 'fs'; +import { ReadStream } from "fs"; -function extend(who: any, from: any, overwrite = true) { // eslint-disable-line @typescript-eslint/no-explicit-any - const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat( - Object.keys(from) - ); - ownProps.forEach(prop => { - if (prop === 'constructor' || from[prop] === undefined) return; - if (who[prop] && overwrite) { - who[`_${prop}`] = who[prop]; - } - if (typeof from[prop] === 'function') who[prop] = from[prop].bind(who); - else who[prop] = from[prop]; - }); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function extend(who: any, from: any, overwrite = true) { + const ownProps = Object.getOwnPropertyNames(Object.getPrototypeOf(from)).concat(Object.keys(from)); + ownProps.forEach((prop) => { + if (prop === "constructor" || from[prop] === undefined) return; + if (who[prop] && overwrite) { + who[`_${prop}`] = who[prop]; + } + if (typeof from[prop] === "function") who[prop] = from[prop].bind(who); + else who[prop] = from[prop]; + }); } function stob(stream: ReadStream): Promise { - return new Promise(resolve => { - const buffers: Buffer[] = []; - stream.on('data', buffers.push.bind(buffers)); + return new Promise((resolve) => { + const buffers: Buffer[] = []; + stream.on("data", buffers.push.bind(buffers)); - stream.on('end', () => { - switch (buffers.length) { - case 0: - resolve(Buffer.allocUnsafe(0)); - break; - case 1: - resolve(buffers[0]); - break; - default: - resolve(Buffer.concat(buffers)); - } + stream.on("end", () => { + switch (buffers.length) { + case 0: + resolve(Buffer.allocUnsafe(0)); + break; + case 1: + resolve(buffers[0]); + break; + default: + resolve(Buffer.concat(buffers)); + } + }); }); - }); } export { extend, stob }; diff --git a/pusher/src/Server/sifrr.server.ts b/pusher/src/Server/sifrr.server.ts index 47fba02c..4ef03721 100644 --- a/pusher/src/Server/sifrr.server.ts +++ b/pusher/src/Server/sifrr.server.ts @@ -1,19 +1,19 @@ -import { parse } from 'query-string'; -import { HttpRequest } from 'uWebSockets.js'; -import App from './server/app'; -import SSLApp from './server/sslapp'; -import * as types from './server/types'; +import { parse } from "query-string"; +import { HttpRequest } from "uWebSockets.js"; +import App from "./server/app"; +import SSLApp from "./server/sslapp"; +import * as types from "./server/types"; const getQuery = (req: HttpRequest) => { - return parse(req.getQuery()); + return parse(req.getQuery()); }; export { App, SSLApp, getQuery }; -export * from './server/types'; +export * from "./server/types"; export default { - App, - SSLApp, - getQuery, - ...types + App, + SSLApp, + getQuery, + ...types, }; diff --git a/pusher/src/Services/AdminApi.ts b/pusher/src/Services/AdminApi.ts index fbd7a070..2cbac52c 100644 --- a/pusher/src/Services/AdminApi.ts +++ b/pusher/src/Services/AdminApi.ts @@ -1,123 +1,147 @@ -import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; +import { ADMIN_API_TOKEN, ADMIN_API_URL } from "../Enum/EnvironmentVariable"; import Axios from "axios"; -import {GameRoomPolicyTypes} from "_Model/PusherRoom"; +import { GameRoomPolicyTypes } from "_Model/PusherRoom"; export interface AdminApiData { - organizationSlug: string - worldSlug: string - roomSlug: string - mapUrlStart: string - tags: string[] - policy_type: number - userUuid: string - messages?: unknown[], - textures: CharacterTexture[] + organizationSlug: string; + worldSlug: string; + roomSlug: string; + mapUrlStart: string; + tags: string[]; + policy_type: number; + userUuid: string; + messages?: unknown[]; + textures: CharacterTexture[]; } export interface MapDetailsData { - roomSlug: string, - mapUrl: string, - policy_type: GameRoomPolicyTypes, - tags: string[], + roomSlug: string; + mapUrl: string; + policy_type: GameRoomPolicyTypes; + tags: string[]; } export interface AdminBannedData { - is_banned: boolean, - message: string + is_banned: boolean; + message: string; } export interface CharacterTexture { - id: number, - level: number, - url: string, - rights: string + id: number; + level: number; + url: string; + rights: string; } export interface FetchMemberDataByUuidResponse { uuid: string; tags: string[]; - visitCardUrl: string|null; + visitCardUrl: string | null; textures: CharacterTexture[]; messages: unknown[]; anonymous?: boolean; } class AdminApi { - - async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise { + async fetchMapDetails( + organizationSlug: string, + worldSlug: string, + roomSlug: string | undefined + ): Promise { if (!ADMIN_API_URL) { - return Promise.reject(new Error('No admin backoffice set!')); + return Promise.reject(new Error("No admin backoffice set!")); } - const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = { + const params: { organizationSlug: string; worldSlug: string; roomSlug?: string } = { organizationSlug, - worldSlug + worldSlug, }; if (roomSlug) { params.roomSlug = roomSlug; } - const res = await Axios.get(ADMIN_API_URL + '/api/map', - { - headers: {"Authorization": `${ADMIN_API_TOKEN}`}, - params - } - ) + const res = await Axios.get(ADMIN_API_URL + "/api/map", { + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + params, + }); return res.data; } async fetchMemberDataByUuid(uuid: string, roomId: string): Promise { if (!ADMIN_API_URL) { - return Promise.reject(new Error('No admin backoffice set!')); + return Promise.reject(new Error("No admin backoffice set!")); } - const res = await Axios.get(ADMIN_API_URL+'/api/room/access', - { params: {uuid, roomId}, headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) + const res = await Axios.get(ADMIN_API_URL + "/api/room/access", { + params: { uuid, roomId }, + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + }); return res.data; } async fetchMemberDataByToken(organizationMemberToken: string): Promise { if (!ADMIN_API_URL) { - return Promise.reject(new Error('No admin backoffice set!')); + return Promise.reject(new Error("No admin backoffice set!")); } //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. - const res = await Axios.get(ADMIN_API_URL+'/api/login-url/'+organizationMemberToken, - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) + const res = await Axios.get(ADMIN_API_URL + "/api/login-url/" + organizationMemberToken, { + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + }); return res.data; } async fetchCheckUserByToken(organizationMemberToken: string): Promise { if (!ADMIN_API_URL) { - return Promise.reject(new Error('No admin backoffice set!')); + return Promise.reject(new Error("No admin backoffice set!")); } //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. - const res = await Axios.get(ADMIN_API_URL+'/api/check-user/'+organizationMemberToken, - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) + const res = await Axios.get(ADMIN_API_URL + "/api/check-user/" + organizationMemberToken, { + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + }); return res.data; } - reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string, reportWorldSlug: string) { - return Axios.post(`${ADMIN_API_URL}/api/report`, { + reportPlayer( + reportedUserUuid: string, + reportedUserComment: string, + reporterUserUuid: string, + reportWorldSlug: string + ) { + return Axios.post( + `${ADMIN_API_URL}/api/report`, + { reportedUserUuid, reportedUserComment, reporterUserUuid, - reportWorldSlug + reportWorldSlug, }, { - headers: {"Authorization": `${ADMIN_API_TOKEN}`} - }); + headers: { Authorization: `${ADMIN_API_TOKEN}` }, + } + ); } - async verifyBanUser(organizationMemberToken: string, ipAddress: string, organization: string, world: string): Promise { + async verifyBanUser( + organizationMemberToken: string, + ipAddress: string, + organization: string, + world: string + ): Promise { if (!ADMIN_API_URL) { - return Promise.reject(new Error('No admin backoffice set!')); + return Promise.reject(new Error("No admin backoffice set!")); } //todo: this call can fail if the corresponding world is not activated or if the token is invalid. Handle that case. - return Axios.get(ADMIN_API_URL + '/api/check-moderate-user/'+organization+'/'+world+'?ipAddress='+ipAddress+'&token='+organizationMemberToken, - {headers: {"Authorization": `${ADMIN_API_TOKEN}`}} + return Axios.get( + ADMIN_API_URL + + "/api/check-moderate-user/" + + organization + + "/" + + world + + "?ipAddress=" + + ipAddress + + "&token=" + + organizationMemberToken, + { headers: { Authorization: `${ADMIN_API_TOKEN}` } } ).then((data) => { return data.data; }); diff --git a/pusher/src/Services/ApiClientRepository.ts b/pusher/src/Services/ApiClientRepository.ts index c1c6bd38..59e181ae 100644 --- a/pusher/src/Services/ApiClientRepository.ts +++ b/pusher/src/Services/ApiClientRepository.ts @@ -1,14 +1,14 @@ /** * A class to get connections to the correct "api" server given a room name. */ -import {RoomManagerClient} from "../Messages/generated/messages_grpc_pb"; -import grpc from 'grpc'; -import crypto from 'crypto'; -import {API_URL} from "../Enum/EnvironmentVariable"; +import { RoomManagerClient } from "../Messages/generated/messages_grpc_pb"; +import grpc from "grpc"; +import crypto from "crypto"; +import { API_URL } from "../Enum/EnvironmentVariable"; import Debug from "debug"; -const debug = Debug('apiClientRespository'); +const debug = Debug("apiClientRespository"); class ApiClientRepository { private roomManagerClients: RoomManagerClient[] = []; @@ -16,23 +16,26 @@ class ApiClientRepository { public constructor(private apiUrls: string[]) {} public async getClient(roomId: string): Promise { - const array = new Uint32Array(crypto.createHash('md5').update(roomId).digest()); + const array = new Uint32Array(crypto.createHash("md5").update(roomId).digest()); const index = array[0] % this.apiUrls.length; let client = this.roomManagerClients[index]; if (client === undefined) { - this.roomManagerClients[index] = client = new RoomManagerClient(this.apiUrls[index], grpc.credentials.createInsecure()); - debug('Mapping room %s to API server %s', roomId, this.apiUrls[index]) + this.roomManagerClients[index] = client = new RoomManagerClient( + this.apiUrls[index], + grpc.credentials.createInsecure() + ); + debug("Mapping room %s to API server %s", roomId, this.apiUrls[index]); } return Promise.resolve(client); } public async getAllClients(): Promise { - return [await this.getClient('')]; + return [await this.getClient("")]; } } -const apiClientRepository = new ApiClientRepository(API_URL.split(',')); +const apiClientRepository = new ApiClientRepository(API_URL.split(",")); export { apiClientRepository }; diff --git a/pusher/src/Services/ArrayHelper.ts b/pusher/src/Services/ArrayHelper.ts index 67321d1b..8af1da9f 100644 --- a/pusher/src/Services/ArrayHelper.ts +++ b/pusher/src/Services/ArrayHelper.ts @@ -1,3 +1,3 @@ -export const arrayIntersect = (array1: string[], array2: string[]) : boolean => { - return array1.filter(value => array2.includes(value)).length > 0; -} \ No newline at end of file +export const arrayIntersect = (array1: string[], array2: string[]): boolean => { + return array1.filter((value) => array2.includes(value)).length > 0; +}; diff --git a/pusher/src/Services/ClientEventsEmitter.ts b/pusher/src/Services/ClientEventsEmitter.ts index 7b888ef6..0f56d55c 100644 --- a/pusher/src/Services/ClientEventsEmitter.ts +++ b/pusher/src/Services/ClientEventsEmitter.ts @@ -1,7 +1,7 @@ -const EventEmitter = require('events'); +const EventEmitter = require("events"); -const clientJoinEvent = 'clientJoin'; -const clientLeaveEvent = 'clientLeave'; +const clientJoinEvent = "clientJoin"; +const clientLeaveEvent = "clientLeave"; class ClientEventsEmitter extends EventEmitter { emitClientJoin(clientUUid: string, roomId: string): void { @@ -11,7 +11,7 @@ class ClientEventsEmitter extends EventEmitter { emitClientLeave(clientUUid: string, roomId: string): void { this.emit(clientLeaveEvent, clientUUid, roomId); } - + registerToClientJoin(callback: (clientUUid: string, roomId: string) => void): void { this.on(clientJoinEvent, callback); } @@ -29,4 +29,4 @@ class ClientEventsEmitter extends EventEmitter { } } -export const clientEventsEmitter = new ClientEventsEmitter(); \ No newline at end of file +export const clientEventsEmitter = new ClientEventsEmitter(); diff --git a/pusher/src/Services/CpuTracker.ts b/pusher/src/Services/CpuTracker.ts index c7d57f3d..3d06ca70 100644 --- a/pusher/src/Services/CpuTracker.ts +++ b/pusher/src/Services/CpuTracker.ts @@ -1,6 +1,6 @@ -import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable"; +import { CPU_OVERHEAT_THRESHOLD } from "../Enum/EnvironmentVariable"; -function secNSec2ms(secNSec: Array|number) { +function secNSec2ms(secNSec: Array | number) { if (Array.isArray(secNSec)) { return secNSec[0] * 1000 + secNSec[1] / 1000000; } @@ -12,17 +12,17 @@ class CpuTracker { private overHeating: boolean = false; constructor() { - let time = process.hrtime.bigint() - let usage = process.cpuUsage() + let time = process.hrtime.bigint(); + let usage = process.cpuUsage(); setInterval(() => { const elapTime = process.hrtime.bigint(); - const elapUsage = process.cpuUsage(usage) - usage = process.cpuUsage() + const elapUsage = process.cpuUsage(usage); + usage = process.cpuUsage(); const elapTimeMS = elapTime - time; - const elapUserMS = secNSec2ms(elapUsage.user) - const elapSystMS = secNSec2ms(elapUsage.system) - this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000) + const elapUserMS = secNSec2ms(elapUsage.user); + const elapSystMS = secNSec2ms(elapUsage.system); + this.cpuPercent = Math.round(((100 * (elapUserMS + elapSystMS)) / Number(elapTimeMS)) * 1000000); time = elapTime; diff --git a/pusher/src/Services/GaugeManager.ts b/pusher/src/Services/GaugeManager.ts index f8af822b..9780d89d 100644 --- a/pusher/src/Services/GaugeManager.ts +++ b/pusher/src/Services/GaugeManager.ts @@ -1,4 +1,4 @@ -import {Counter, Gauge} from "prom-client"; +import { Counter, Gauge } from "prom-client"; //this class should manage all the custom metrics used by prometheus class GaugeManager { @@ -9,25 +9,25 @@ class GaugeManager { constructor() { this.nbClientsGauge = new Gauge({ - name: 'workadventure_nb_sockets', - help: 'Number of connected sockets', - labelNames: [ ] + name: "workadventure_nb_sockets", + help: "Number of connected sockets", + labelNames: [], }); this.nbClientsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_clients_per_room', - help: 'Number of clients per room', - labelNames: [ 'room' ] + name: "workadventure_nb_clients_per_room", + help: "Number of clients per room", + labelNames: ["room"], }); this.nbGroupsPerRoomCounter = new Counter({ - name: 'workadventure_counter_groups_per_room', - help: 'Counter of groups per room', - labelNames: [ 'room' ] + name: "workadventure_counter_groups_per_room", + help: "Counter of groups per room", + labelNames: ["room"], }); this.nbGroupsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_groups_per_room', - help: 'Number of groups per room', - labelNames: [ 'room' ] + name: "workadventure_nb_groups_per_room", + help: "Number of groups per room", + labelNames: ["room"], }); } @@ -42,13 +42,13 @@ class GaugeManager { } incNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomCounter.inc({ room: roomId }) - this.nbGroupsPerRoomGauge.inc({ room: roomId }) + this.nbGroupsPerRoomCounter.inc({ room: roomId }); + this.nbGroupsPerRoomGauge.inc({ room: roomId }); } - + decNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomGauge.dec({ room: roomId }) + this.nbGroupsPerRoomGauge.dec({ room: roomId }); } } -export const gaugeManager = new GaugeManager(); \ No newline at end of file +export const gaugeManager = new GaugeManager(); diff --git a/pusher/src/Services/IoSocketHelpers.ts b/pusher/src/Services/IoSocketHelpers.ts index e90e0874..2da7c430 100644 --- a/pusher/src/Services/IoSocketHelpers.ts +++ b/pusher/src/Services/IoSocketHelpers.ts @@ -1,6 +1,6 @@ -import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; -import {BatchMessage, ErrorMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb"; -import {WebSocket} from "uWebSockets.js"; +import { ExSocketInterface } from "_Model/Websocket/ExSocketInterface"; +import { BatchMessage, ErrorMessage, ServerToClientMessage, SubMessage } from "../Messages/generated/messages_pb"; +import { WebSocket } from "uWebSockets.js"; export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { socket.batchedMessages.addPayload(payload); @@ -33,4 +33,3 @@ export function emitError(Client: WebSocket, message: string): void { } console.warn(message); } - diff --git a/pusher/src/Services/JWTTokenManager.ts b/pusher/src/Services/JWTTokenManager.ts index 68d5488a..2e82929e 100644 --- a/pusher/src/Services/JWTTokenManager.ts +++ b/pusher/src/Services/JWTTokenManager.ts @@ -1,73 +1,77 @@ -import {ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable"; -import {uuid} from "uuidv4"; +import { ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable"; +import { uuid } from "uuidv4"; import Jwt from "jsonwebtoken"; -import {TokenInterface} from "../Controller/AuthenticateController"; -import {adminApi, AdminBannedData} from "../Services/AdminApi"; +import { TokenInterface } from "../Controller/AuthenticateController"; +import { adminApi, AdminBannedData } from "../Services/AdminApi"; class JWTTokenManager { - public createJWTToken(userUuid: string) { - return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '200d'}); //todo: add a mechanic to refresh or recreate token + return Jwt.sign({ userUuid: userUuid }, SECRET_KEY, { expiresIn: "200d" }); //todo: add a mechanic to refresh or recreate token } public async getUserUuidFromToken(token: unknown, ipAddress?: string, room?: string): Promise { - if (!token) { - throw new Error('An authentication error happened, a user tried to connect without a token.'); + throw new Error("An authentication error happened, a user tried to connect without a token."); } - if (typeof(token) !== "string") { - throw new Error('Token is expected to be a string'); + if (typeof token !== "string") { + throw new Error("Token is expected to be a string"); } - - if(token === 'test') { + if (token === "test") { if (ALLOW_ARTILLERY) { return uuid(); } else { - throw new Error("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"); + throw new Error( + "In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'" + ); } } return new Promise((resolve, reject) => { - Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => { + Jwt.verify(token, SECRET_KEY, {}, (err, tokenDecoded) => { const tokenInterface = tokenDecoded as TokenInterface; if (err) { - console.error('An authentication error happened, invalid JsonWebToken.', err); - reject(new Error('An authentication error happened, invalid JsonWebToken. ' + err.message)); + console.error("An authentication error happened, invalid JsonWebToken.", err); + reject(new Error("An authentication error happened, invalid JsonWebToken. " + err.message)); return; } if (tokenDecoded === undefined) { - console.error('Empty token found.'); - reject(new Error('Empty token found.')); + console.error("Empty token found."); + reject(new Error("Empty token found.")); return; } //verify token if (!this.isValidToken(tokenInterface)) { - reject(new Error('Authentication error, invalid token structure.')); + reject(new Error("Authentication error, invalid token structure.")); return; } if (ADMIN_API_URL) { //verify user in admin let promise = new Promise((resolve) => resolve()); - if(ipAddress && room) { + if (ipAddress && room) { promise = this.verifyBanUser(tokenInterface.userUuid, ipAddress, room); } - promise.then(() => { - adminApi.fetchCheckUserByToken(tokenInterface.userUuid).then(() => { - resolve(tokenInterface.userUuid); - }).catch((err) => { - //anonymous user - if (err.response && err.response.status && err.response.status === 404) { - resolve(tokenInterface.userUuid); - return; - } + promise + .then(() => { + adminApi + .fetchCheckUserByToken(tokenInterface.userUuid) + .then(() => { + resolve(tokenInterface.userUuid); + }) + .catch((err) => { + //anonymous user + if (err.response && err.response.status && err.response.status === 404) { + resolve(tokenInterface.userUuid); + return; + } + reject(err); + }); + }) + .catch((err) => { reject(err); }); - }).catch((err) => { - reject(err); - }); } else { resolve(tokenInterface.userUuid); } @@ -76,30 +80,32 @@ class JWTTokenManager { } private verifyBanUser(userUuid: string, ipAddress: string, room: string): Promise { - const parts = room.split('/'); - if (parts.length < 3 || parts[0] !== '@') { + const parts = room.split("/"); + if (parts.length < 3 || parts[0] !== "@") { return Promise.resolve({ is_banned: false, - message: '' + message: "", }); } const organization = parts[1]; const world = parts[2]; - return adminApi.verifyBanUser(userUuid, ipAddress, organization, world).then((data: AdminBannedData) => { - if (data && data.is_banned) { - throw new Error('User was banned'); - } - return data; - }).catch((err) => { - throw err; - }); + return adminApi + .verifyBanUser(userUuid, ipAddress, organization, world) + .then((data: AdminBannedData) => { + if (data && data.is_banned) { + throw new Error("User was banned"); + } + return data; + }) + .catch((err) => { + throw err; + }); } private isValidToken(token: object): token is TokenInterface { - return !(typeof((token as TokenInterface).userUuid) !== 'string'); + return !(typeof (token as TokenInterface).userUuid !== "string"); } - } export const jwtTokenManager = new JWTTokenManager(); diff --git a/pusher/src/Services/SocketManager.ts b/pusher/src/Services/SocketManager.ts index 319d2b5e..8a0d3673 100644 --- a/pusher/src/Services/SocketManager.ts +++ b/pusher/src/Services/SocketManager.ts @@ -1,5 +1,5 @@ -import {PusherRoom} from "../Model/PusherRoom"; -import {CharacterLayer, ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; +import { PusherRoom } from "../Model/PusherRoom"; +import { CharacterLayer, ExSocketInterface } from "../Model/Websocket/ExSocketInterface"; import { GroupDeleteMessage, ItemEventMessage, @@ -31,21 +31,21 @@ import { RefreshRoomMessage, EmotePromptMessage, } from "../Messages/generated/messages_pb"; -import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; -import {JITSI_ISS, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable"; -import {adminApi, CharacterTexture} from "./AdminApi"; -import {emitInBatch} from "./IoSocketHelpers"; +import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils"; +import { JITSI_ISS, SECRET_JITSI_KEY } from "../Enum/EnvironmentVariable"; +import { adminApi, CharacterTexture } from "./AdminApi"; +import { emitInBatch } from "./IoSocketHelpers"; import Jwt from "jsonwebtoken"; -import {JITSI_URL} from "../Enum/EnvironmentVariable"; -import {clientEventsEmitter} from "./ClientEventsEmitter"; -import {gaugeManager} from "./GaugeManager"; -import {apiClientRepository} from "./ApiClientRepository"; -import {GroupDescriptor, UserDescriptor, ZoneEventListener} from "_Model/Zone"; +import { JITSI_URL } from "../Enum/EnvironmentVariable"; +import { clientEventsEmitter } from "./ClientEventsEmitter"; +import { gaugeManager } from "./GaugeManager"; +import { apiClientRepository } from "./ApiClientRepository"; +import { GroupDescriptor, UserDescriptor, ZoneEventListener } from "_Model/Zone"; import Debug from "debug"; -import {ExAdminSocketInterface} from "_Model/Websocket/ExAdminSocketInterface"; -import {WebSocket} from "uWebSockets.js"; +import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface"; +import { WebSocket } from "uWebSockets.js"; -const debug = Debug('socket'); +const debug = Debug("socket"); interface AdminSocketRoomsList { [index: string]: number; @@ -55,12 +55,11 @@ interface AdminSocketUsersList { } export interface AdminSocketData { - rooms: AdminSocketRoomsList, - users: AdminSocketUsersList, + rooms: AdminSocketRoomsList; + users: AdminSocketUsersList; } export class SocketManager implements ZoneEventListener { - private rooms: Map = new Map(); private sockets: Map = new Map(); @@ -78,47 +77,53 @@ export class SocketManager implements ZoneEventListener { const adminRoomStream = apiClient.adminRoom(); client.adminConnection = adminRoomStream; - adminRoomStream.on('data', (message: ServerToAdminClientMessage) => { - - if (message.hasUserjoinedroom()) { - const userJoinedRoomMessage = message.getUserjoinedroom() as UserJoinedRoomMessage; - if (!client.disconnecting) { - client.send(JSON.stringify({ - type: 'MemberJoin', - data: { - uuid: userJoinedRoomMessage.getUuid(), - name: userJoinedRoomMessage.getName(), - ipAddress: userJoinedRoomMessage.getIpaddress(), - roomId: roomId, - } - })); + adminRoomStream + .on("data", (message: ServerToAdminClientMessage) => { + if (message.hasUserjoinedroom()) { + const userJoinedRoomMessage = message.getUserjoinedroom() as UserJoinedRoomMessage; + if (!client.disconnecting) { + client.send( + JSON.stringify({ + type: "MemberJoin", + data: { + uuid: userJoinedRoomMessage.getUuid(), + name: userJoinedRoomMessage.getName(), + ipAddress: userJoinedRoomMessage.getIpaddress(), + roomId: roomId, + }, + }) + ); + } + } else if (message.hasUserleftroom()) { + const userLeftRoomMessage = message.getUserleftroom() as UserLeftRoomMessage; + if (!client.disconnecting) { + client.send( + JSON.stringify({ + type: "MemberLeave", + data: { + uuid: userLeftRoomMessage.getUuid(), + }, + }) + ); + } + } else { + throw new Error("Unexpected admin message"); } - } else if (message.hasUserleftroom()) { - const userLeftRoomMessage = message.getUserleftroom() as UserLeftRoomMessage; + }) + .on("end", () => { + console.warn("Admin connection lost to back server"); + // Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. if (!client.disconnecting) { - client.send(JSON.stringify({ - type: 'MemberLeave', - data: { - uuid: userLeftRoomMessage.getUuid() - } - })); + this.closeWebsocketConnection(client, 1011, "Connection lost to back server"); } - } else { - throw new Error('Unexpected admin message'); - } - }).on('end', () => { - console.warn('Admin connection lost to back server'); - // Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. - if (!client.disconnecting) { - this.closeWebsocketConnection(client, 1011, 'Connection lost to back server'); - } - console.log('A user left'); - }).on('error', (err: Error) => { - console.error('Error in connection to back server:', err); - if (!client.disconnecting) { - this.closeWebsocketConnection(client, 1011, 'Error while connecting to back server'); - } - }); + console.log("A user left"); + }) + .on("error", (err: Error) => { + console.error("Error in connection to back server:", err); + if (!client.disconnecting) { + this.closeWebsocketConnection(client, 1011, "Error while connecting to back server"); + } + }); const message = new AdminPusherToBackMessage(); message.setSubscribetoroom(roomId); @@ -126,14 +131,14 @@ export class SocketManager implements ZoneEventListener { adminRoomStream.write(message); } - leaveAdminRoom(socket : ExAdminSocketInterface) { + leaveAdminRoom(socket: ExAdminSocketInterface) { if (socket.adminConnection) { socket.adminConnection.end(); } } - getAdminSocketDataFor(roomId:string): AdminSocketData { - throw new Error('Not reimplemented yet'); + getAdminSocketDataFor(roomId: string): AdminSocketData { + throw new Error("Not reimplemented yet"); /*const data:AdminSocketData = { rooms: {}, users: {}, @@ -153,7 +158,6 @@ export class SocketManager implements ZoneEventListener { async handleJoinRoom(client: ExSocketInterface): Promise { const viewport = client.viewport; try { - const joinRoomMessage = new JoinRoomMessage(); joinRoomMessage.setUseruuid(client.userUuid); joinRoomMessage.setIpaddress(client.IPAddress); @@ -176,46 +180,49 @@ export class SocketManager implements ZoneEventListener { joinRoomMessage.addCharacterlayer(characterLayerMessage); } - - console.log('Calling joinRoom') + console.log("Calling joinRoom"); const apiClient = await apiClientRepository.getClient(client.roomId); const streamToPusher = apiClient.joinRoom(); clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId); client.backConnection = streamToPusher; - streamToPusher.on('data', (message: ServerToClientMessage) => { - if (message.hasRoomjoinedmessage()) { - client.userId = (message.getRoomjoinedmessage() as RoomJoinedMessage).getCurrentuserid(); - // TODO: do we need this.sockets anymore? - this.sockets.set(client.userId, client); + streamToPusher + .on("data", (message: ServerToClientMessage) => { + if (message.hasRoomjoinedmessage()) { + client.userId = (message.getRoomjoinedmessage() as RoomJoinedMessage).getCurrentuserid(); + // TODO: do we need this.sockets anymore? + this.sockets.set(client.userId, client); - // If this is the first message sent, send back the viewport. - this.handleViewport(client, viewport); - } - - if (message.hasRefreshroommessage()) { - const refreshMessage:RefreshRoomMessage = message.getRefreshroommessage() as unknown as RefreshRoomMessage; - this.refreshRoomData(refreshMessage.getRoomid(), refreshMessage.getVersionnumber()) - } + // If this is the first message sent, send back the viewport. + this.handleViewport(client, viewport); + } - // Let's pass data over from the back to the client. - if (!client.disconnecting) { - client.send(message.serializeBinary().buffer, true); - } - }).on('end', () => { - console.warn('Connection lost to back server'); - // Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. - if (!client.disconnecting) { - this.closeWebsocketConnection(client, 1011, 'Connection lost to back server'); - } - console.log('A user left'); - }).on('error', (err: Error) => { - console.error('Error in connection to back server:', err); - if (!client.disconnecting) { - this.closeWebsocketConnection(client, 1011, 'Error while connecting to back server'); - } - }); + if (message.hasRefreshroommessage()) { + const refreshMessage: RefreshRoomMessage = + message.getRefreshroommessage() as unknown as RefreshRoomMessage; + this.refreshRoomData(refreshMessage.getRoomid(), refreshMessage.getVersionnumber()); + } + + // Let's pass data over from the back to the client. + if (!client.disconnecting) { + client.send(message.serializeBinary().buffer, true); + } + }) + .on("end", () => { + console.warn("Connection lost to back server"); + // Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. + if (!client.disconnecting) { + this.closeWebsocketConnection(client, 1011, "Connection lost to back server"); + } + console.log("A user left"); + }) + .on("error", (err: Error) => { + console.error("Error in connection to back server:", err); + if (!client.disconnecting) { + this.closeWebsocketConnection(client, 1011, "Error while connecting to back server"); + } + }); const pusherToBackMessage = new PusherToBackMessage(); pusherToBackMessage.setJoinroommessage(joinRoomMessage); @@ -226,7 +233,7 @@ export class SocketManager implements ZoneEventListener { } } - private closeWebsocketConnection(client: ExSocketInterface|ExAdminSocketInterface, code: number, reason: string) { + private closeWebsocketConnection(client: ExSocketInterface | ExAdminSocketInterface, code: number, reason: string) { client.disconnecting = true; //this.leaveRoom(client); //client.close(); @@ -257,15 +264,13 @@ export class SocketManager implements ZoneEventListener { const viewport = userMovesMessage.getViewport(); if (viewport === undefined) { - throw new Error('Missing viewport in UserMovesMessage'); + throw new Error("Missing viewport in UserMovesMessage"); } // Now, we need to listen to the correct viewport. - this.handleViewport(client, viewport.toObject()) + this.handleViewport(client, viewport.toObject()); } - - onEmote(emoteMessage: EmoteEventMessage, listener: ExSocketInterface): void { const subMessage = new SubMessage(); subMessage.setEmoteeventmessage(emoteMessage); @@ -299,11 +304,16 @@ export class SocketManager implements ZoneEventListener { try { const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); if (!reportedSocket) { - throw 'reported socket user not found'; + throw "reported socket user not found"; } //TODO report user on admin application - //todo: move to back because this fail if the reported player is in another pusher. - await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid, client.roomId.split('/')[2]) + //todo: move to back because this fail if the reported player is in another pusher. + await adminApi.reportPlayer( + reportedSocket.userUuid, + reportPlayerMessage.getReportcomment(), + client.userUuid, + client.roomId.split("/")[2] + ); } catch (e) { console.error('An error occurred on "handleReportMessage"'); console.error(e); @@ -325,14 +335,14 @@ export class SocketManager implements ZoneEventListener { } private searchClientByIdOrFail(userId: number): ExSocketInterface { - const client: ExSocketInterface|undefined = this.sockets.get(userId); + const client: ExSocketInterface | undefined = this.sockets.get(userId); if (client === undefined) { throw new Error("Could not find user with id " + userId); } return client; } - leaveRoom(socket : ExSocketInterface) { + leaveRoom(socket: ExSocketInterface) { // leave previous room and world try { if (socket.roomId) { @@ -340,15 +350,15 @@ export class SocketManager implements ZoneEventListener { //user leaves room const room: PusherRoom | undefined = this.rooms.get(socket.roomId); if (room) { - debug('Leaving room %s.', socket.roomId); - + debug("Leaving room %s.", socket.roomId); + room.leave(socket); if (room.isEmpty()) { this.rooms.delete(socket.roomId); - debug('Room %s is empty. Deleting.', socket.roomId); + debug("Room %s is empty. Deleting.", socket.roomId); } } else { - console.error('Could not find the GameRoom the user is leaving!'); + console.error("Could not find the GameRoom the user is leaving!"); } //user leave previous room //Client.leave(Client.roomId); @@ -356,7 +366,7 @@ export class SocketManager implements ZoneEventListener { //delete Client.roomId; this.sockets.delete(socket.userId); clientEventsEmitter.emitClientLeave(socket.userUuid, socket.roomId); - console.log('A user left (', this.sockets.size, ' connected users)'); + console.log("A user left (", this.sockets.size, " connected users)"); } } } finally { @@ -368,27 +378,27 @@ export class SocketManager implements ZoneEventListener { async getOrCreateRoom(roomId: string): Promise { //check and create new world for a room - let world = this.rooms.get(roomId) - if(world === undefined){ + let world = this.rooms.get(roomId); + if (world === undefined) { world = new PusherRoom(roomId, this); if (!world.public) { await this.updateRoomWithAdminData(world); } this.rooms.set(roomId, world); } - return Promise.resolve(world) + return Promise.resolve(world); } public async updateRoomWithAdminData(world: PusherRoom): Promise { - const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug) + const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug); world.tags = data.tags; world.policyType = Number(data.policy_type); } emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) { - if (!client.tags.includes('admin')) { + if (!client.tags.includes("admin")) { //In case of xss injection, we just kill the connection. - throw 'Client is not an admin!'; + throw "Client is not an admin!"; } const pusherToBackMessage = new PusherToBackMessage(); pusherToBackMessage.setPlayglobalmessage(playglobalmessage); @@ -399,44 +409,48 @@ export class SocketManager implements ZoneEventListener { public getWorlds(): Map { return this.rooms; } - + searchClientByUuid(uuid: string): ExSocketInterface | null { - for(const socket of this.sockets.values()){ - if(socket.userUuid === uuid){ + for (const socket of this.sockets.values()) { + if (socket.userUuid === uuid) { return socket; } } return null; } - public handleQueryJitsiJwtMessage(client: ExSocketInterface, queryJitsiJwtMessage: QueryJitsiJwtMessage) { try { const room = queryJitsiJwtMessage.getJitsiroom(); const tag = queryJitsiJwtMessage.getTag(); // FIXME: this is not secure. We should load the JSON for the current room and check rights associated to room instead. - if (SECRET_JITSI_KEY === '') { - throw new Error('You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi.'); + if (SECRET_JITSI_KEY === "") { + throw new Error( + "You must set the SECRET_JITSI_KEY key to the secret to generate JWT tokens for Jitsi." + ); } // Let's see if the current client has const isAdmin = client.tags.includes(tag); - const jwt = Jwt.sign({ - "aud": "jitsi", - "iss": JITSI_ISS, - "sub": JITSI_URL, - "room": room, - "moderator": isAdmin - }, SECRET_JITSI_KEY, { - expiresIn: '1d', - algorithm: "HS256", - header: - { - "alg": "HS256", - "typ": "JWT" - } - }); + const jwt = Jwt.sign( + { + aud: "jitsi", + iss: JITSI_ISS, + sub: JITSI_URL, + room: room, + moderator: isAdmin, + }, + SECRET_JITSI_KEY, + { + expiresIn: "1d", + algorithm: "HS256", + header: { + alg: "HS256", + typ: "JWT", + }, + } + ); const sendJitsiJwtMessage = new SendJitsiJwtMessage(); sendJitsiJwtMessage.setJitsiroom(room); @@ -447,7 +461,7 @@ export class SocketManager implements ZoneEventListener { client.send(serverToClientMessage.serializeBinary().buffer, true); } catch (e) { - console.error('An error occured while generating the Jitsi JWT token: ', e); + console.error("An error occured while generating the Jitsi JWT token: ", e); } } @@ -471,7 +485,7 @@ export class SocketManager implements ZoneEventListener { backAdminMessage.setType(type); backConnection.sendAdminMessage(backAdminMessage, (error) => { if (error !== null) { - console.error('Error while sending admin message', error); + console.error("Error while sending admin message", error); } }); } @@ -496,7 +510,7 @@ export class SocketManager implements ZoneEventListener { banMessage.setType(type); backConnection.ban(banMessage, (error) => { if (error !== null) { - console.error('Error while sending admin message', error); + console.error("Error while sending admin message", error); } }); } @@ -504,25 +518,28 @@ export class SocketManager implements ZoneEventListener { /** * Merges the characterLayers received from the front (as an array of string) with the custom textures from the back. */ - static mergeCharacterLayersAndCustomTextures(characterLayers: string[], memberTextures: CharacterTexture[]): CharacterLayer[] { + static mergeCharacterLayersAndCustomTextures( + characterLayers: string[], + memberTextures: CharacterTexture[] + ): CharacterLayer[] { const characterLayerObjs: CharacterLayer[] = []; for (const characterLayer of characterLayers) { - if (characterLayer.startsWith('customCharacterTexture')) { + if (characterLayer.startsWith("customCharacterTexture")) { const customCharacterLayerId: number = +characterLayer.substr(22); for (const memberTexture of memberTextures) { if (memberTexture.id == customCharacterLayerId) { characterLayerObjs.push({ name: characterLayer, - url: memberTexture.url - }) + url: memberTexture.url, + }); break; } } } else { characterLayerObjs.push({ name: characterLayer, - url: undefined - }) + url: undefined, + }); } } return characterLayerObjs; @@ -572,7 +589,7 @@ export class SocketManager implements ZoneEventListener { emitInBatch(listener, subMessage); } - + public emitWorldFullMessage(client: WebSocket) { const errorMessage = new WorldFullMessage(); @@ -594,9 +611,9 @@ export class SocketManager implements ZoneEventListener { private refreshRoomData(roomId: string, versionNumber: number): void { const room = this.rooms.get(roomId); - //this function is run for every users connected to the room, so we need to make sure the room wasn't already refreshed. + //this function is run for every users connected to the room, so we need to make sure the room wasn't already refreshed. if (!room || !room.needsUpdate(versionNumber)) return; - + this.updateRoomWithAdminData(room); }