From e6bd655527692ce1962bbc2a0adf9dac82d35db7 Mon Sep 17 00:00:00 2001 From: arp Date: Thu, 15 Oct 2020 17:25:16 +0200 Subject: [PATCH] move most of the logic of IOSocketController into a dedicated class --- back/src/App.ts | 4 +- back/src/Controller/DebugController.ts | 5 +- back/src/Controller/IoSocketController.ts | 773 ++------------------ back/src/Controller/PrometheusController.ts | 2 +- back/src/Services/AdminApi.ts | 16 +- back/src/Services/IoSocketHelpers.ts | 50 ++ back/src/Services/SocketManager.ts | 598 +++++++++++++++ 7 files changed, 713 insertions(+), 735 deletions(-) create mode 100644 back/src/Services/IoSocketHelpers.ts create mode 100644 back/src/Services/SocketManager.ts diff --git a/back/src/App.ts b/back/src/App.ts index 6430251a..42659aad 100644 --- a/back/src/App.ts +++ b/back/src/App.ts @@ -24,8 +24,8 @@ class App { this.authenticateController = new AuthenticateController(this.app); this.fileController = new FileController(this.app); this.mapController = new MapController(this.app); - this.prometheusController = new PrometheusController(this.app, this.ioSocketController); - this.debugController = new DebugController(this.app, this.ioSocketController); + this.prometheusController = new PrometheusController(this.app); + this.debugController = new DebugController(this.app); } } diff --git a/back/src/Controller/DebugController.ts b/back/src/Controller/DebugController.ts index e77b28f3..af2db139 100644 --- a/back/src/Controller/DebugController.ts +++ b/back/src/Controller/DebugController.ts @@ -4,9 +4,10 @@ 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, private ioSocketController: IoSocketController) { + constructor(private App : App) { this.getDump(); } @@ -20,7 +21,7 @@ export class DebugController { } return res.writeStatus('200 OK').writeHeader('Content-Type', 'application/json').end(stringify( - this.ioSocketController.getWorlds(), + socketManager.getWorlds(), (key: unknown, value: unknown) => { if(value instanceof Map) { const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any diff --git a/back/src/Controller/IoSocketController.ts b/back/src/Controller/IoSocketController.ts index 2172ff38..1b690754 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -1,113 +1,48 @@ import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." -import {MINIMUM_DISTANCE, GROUP_RADIUS, ADMIN_API_URL, ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." -import {GameRoom, GameRoomPolicyTypes} from "../Model/GameRoom"; -import {Group} from "../Model/Group"; -import {User} from "../Model/User"; -import {isSetPlayerDetailsMessage,} from "../Model/Websocket/SetPlayerDetailsMessage"; -import {Gauge} from "prom-client"; +import {GameRoomPolicyTypes} from "../Model/GameRoom"; import {PointInterface} from "../Model/Websocket/PointInterface"; -import {Movable} from "../Model/Movable"; import { - PositionMessage, SetPlayerDetailsMessage, SubMessage, - UserMovedMessage, BatchMessage, - GroupUpdateMessage, - PointMessage, - GroupDeleteMessage, - UserJoinedMessage, - UserLeftMessage, ItemEventMessage, ViewportMessage, ClientToServerMessage, - ErrorMessage, - RoomJoinedMessage, - ItemStateMessage, - ServerToClientMessage, SilentMessage, - WebRtcSignalToClientMessage, WebRtcSignalToServerMessage, - WebRtcStartMessage, - WebRtcDisconnectMessage, PlayGlobalMessage, - ReportPlayerMessage + ReportPlayerMessage, } from "../Messages/generated/messages_pb"; import {UserMovesMessage} from "../Messages/generated/messages_pb"; -import Direction = PositionMessage.Direction; -import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {TemplatedApp} from "uWebSockets.js" import {parse} from "query-string"; -import {cpuTracker} from "../Services/CpuTracker"; -import {ViewportInterface} from "../Model/Websocket/ViewportMessage"; import {jwtTokenManager} from "../Services/JWTTokenManager"; import {adminApi} from "../Services/AdminApi"; -import Axios from "axios"; -import {PositionInterface} from "../Model/PositionInterface"; - -function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { - socket.batchedMessages.addPayload(payload); - - if (socket.batchTimeout === null) { - socket.batchTimeout = setTimeout(() => { - if (socket.disconnecting) { - return; - } - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setBatchmessage(socket.batchedMessages); - - socket.send(serverToClientMessage.serializeBinary().buffer, true); - socket.batchedMessages = new BatchMessage(); - socket.batchTimeout = null; - }, 100); - } - - // If we send a message, we don't need to keep the connection alive - resetPing(socket); -} - -/** - * Schedule a ping to keep the connection open. - * If a ping is already set, the timeout of the ping is reset. - */ -function resetPing(ws: ExSocketInterface): void { - if (ws.pingTimeout) { - clearTimeout(ws.pingTimeout); - } - ws.pingTimeout = setTimeout(() => { - if (ws.disconnecting) { - return; - } - ws.ping(); - resetPing(ws); - }, 29000); -} +import {socketManager} from "../Services/SocketManager"; +import {emitInBatch, resetPing} from "../Services/IoSocketHelpers"; export class IoSocketController { - private Worlds: Map = new Map(); - private sockets: Map = new Map(); - private nbClientsGauge: Gauge; - private nbClientsPerRoomGauge: Gauge; private nextUserId: number = 1; constructor(private readonly app: TemplatedApp) { - - this.nbClientsGauge = new Gauge({ - 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' ] - }); - this.ioConnection(); + this.adminRoomSocket(); } - + adminRoomSocket() { + /*this.app.ws('/admin/rooms', { + open: (ws) => { + console.log('o', ws) + ws.send('Hello'); + }, + message: (ws, arrayBuffer, isBinary): void => { + console.log('m', ws) + }, + close: (ws, code, message) => { + console.log('close'); + } + })*/ + } ioConnection() { this.app.ws('/room', { @@ -165,7 +100,7 @@ export class IoSocketController { const userUuid = await jwtTokenManager.getUserUuidFromToken(token); let memberTags: string[] = []; - const room = await this.getOrCreateRoom(roomId); + const room = await socketManager.getOrCreateRoom(roomId); if (!room.anonymous && room.policyType !== GameRoomPolicyTypes.ANONYMUS_POLICY) { try { const userData = await adminApi.fetchMemberDataByUuid(userUuid); @@ -229,32 +164,9 @@ export class IoSocketController { }, /* Handlers */ open: (ws) => { - const client : ExSocketInterface = ws as ExSocketInterface; - client.userId = this.nextUserId; - this.nextUserId++; - client.userUuid = ws.userUuid; - client.token = ws.token; - client.batchedMessages = new BatchMessage(); - client.batchTimeout = null; - client.emitInBatch = (payload: SubMessage): void => { - emitInBatch(client, payload); - } - client.disconnecting = false; - - client.name = ws.name; - client.tags = ws.tags; - client.characterLayers = ws.characterLayers; - client.roomId = ws.roomId; - - this.sockets.set(client.userId, client); - - // Let's log server load when a user joins - this.nbClientsGauge.inc(); - console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)'); - // Let's join the room - this.handleJoinRoom(client, client.position, client.viewport); - + const client = this.initClient(ws); //todo: into the upgrade instead? + socketManager.handleJoinRoom(client); resetPing(client); }, message: (ws, arrayBuffer, isBinary): void => { @@ -262,23 +174,23 @@ export class IoSocketController { const message = ClientToServerMessage.deserializeBinary(new Uint8Array(arrayBuffer)); if (message.hasViewportmessage()) { - this.handleViewport(client, message.getViewportmessage() as ViewportMessage); + socketManager.handleViewport(client, message.getViewportmessage() as ViewportMessage); } else if (message.hasUsermovesmessage()) { - this.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage); + socketManager.handleUserMovesMessage(client, message.getUsermovesmessage() as UserMovesMessage); } else if (message.hasSetplayerdetailsmessage()) { - this.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage); + socketManager.handleSetPlayerDetails(client, message.getSetplayerdetailsmessage() as SetPlayerDetailsMessage); } else if (message.hasSilentmessage()) { - this.handleSilentMessage(client, message.getSilentmessage() as SilentMessage); + socketManager.handleSilentMessage(client, message.getSilentmessage() as SilentMessage); } else if (message.hasItemeventmessage()) { - this.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage); + socketManager.handleItemEvent(client, message.getItemeventmessage() as ItemEventMessage); } else if (message.hasWebrtcsignaltoservermessage()) { - this.emitVideo(client, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitVideo(client, message.getWebrtcsignaltoservermessage() as WebRtcSignalToServerMessage); } else if (message.hasWebrtcscreensharingsignaltoservermessage()) { - this.emitScreenSharing(client, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); + socketManager.emitScreenSharing(client, message.getWebrtcscreensharingsignaltoservermessage() as WebRtcSignalToServerMessage); } else if (message.hasPlayglobalmessage()) { - this.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage); + socketManager.emitPlayGlobalMessage(client, message.getPlayglobalmessage() as PlayGlobalMessage); } else if (message.hasReportplayermessage()){ - this.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage); + socketManager.handleReportMessage(client, message.getReportplayermessage() as ReportPlayerMessage); } /* Ok is false if backpressure was built up, wait for drain */ @@ -292,622 +204,33 @@ export class IoSocketController { try { Client.disconnecting = true; //leave room - this.leaveRoom(Client); + socketManager.leaveRoom(Client); } catch (e) { console.error('An error occurred on "disconnect"'); console.error(e); } - - this.sockets.delete(Client.userId); - - // Let's log server load when a user leaves - this.nbClientsGauge.dec(); - console.log('A user left (', this.sockets.size, ' connected users)'); } }) } - private emitError(Client: ExSocketInterface, message: string): void { - const errorMessage = new ErrorMessage(); - errorMessage.setMessage(message); - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setErrormessage(errorMessage); - - if (!Client.disconnecting) { - Client.send(serverToClientMessage.serializeBinary().buffer, true); + //eslint-disable-next-line @typescript-eslint/no-explicit-any + private initClient(ws: any): ExSocketInterface { + const client : ExSocketInterface = ws; + client.userId = this.nextUserId; + this.nextUserId++; + client.userUuid = ws.userUuid; + client.token = ws.token; + client.batchedMessages = new BatchMessage(); + client.batchTimeout = null; + client.emitInBatch = (payload: SubMessage): void => { + emitInBatch(client, payload); } - console.warn(message); - } + client.disconnecting = false; - private handleJoinRoom(client: ExSocketInterface, position: PointInterface, viewport: ViewportInterface): void { - try { - //join new previous room - const gameRoom = this.joinRoom(client, position); - - const things = gameRoom.setViewport(client, viewport); - - const roomJoinedMessage = new RoomJoinedMessage(); - - for (const thing of things) { - if (thing instanceof User) { - const player: ExSocketInterface|undefined = this.sockets.get(thing.id); - if (player === undefined) { - console.warn('Something went wrong. The World contains a user "'+thing.id+"' but this user does not exist in the sockets list!"); - continue; - } - - const userJoinedMessage = new UserJoinedMessage(); - userJoinedMessage.setUserid(thing.id); - userJoinedMessage.setName(player.name); - userJoinedMessage.setCharacterlayersList(player.characterLayers); - userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(player.position)); - - roomJoinedMessage.addUser(userJoinedMessage); - } else if (thing instanceof Group) { - const groupUpdateMessage = new GroupUpdateMessage(); - groupUpdateMessage.setGroupid(thing.getId()); - groupUpdateMessage.setPosition(ProtobufUtils.toPointMessage(thing.getPosition())); - - roomJoinedMessage.addGroup(groupUpdateMessage); - } else { - console.error("Unexpected type for Movable returned by setViewport"); - } - } - - for (const [itemId, item] of gameRoom.getItemsState().entries()) { - const itemStateMessage = new ItemStateMessage(); - itemStateMessage.setItemid(itemId); - itemStateMessage.setStatejson(JSON.stringify(item)); - - roomJoinedMessage.addItem(itemStateMessage); - } - - roomJoinedMessage.setCurrentuserid(client.userId); - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage); - - if (!client.disconnecting) { - client.send(serverToClientMessage.serializeBinary().buffer, true); - } - } catch (e) { - console.error('An error occurred on "join_room" event'); - console.error(e); - } - } - - private handleViewport(client: ExSocketInterface, viewportMessage: ViewportMessage) { - try { - const viewport = viewportMessage.toObject(); - - client.viewport = viewport; - - const world = this.Worlds.get(client.roomId); - if (!world) { - console.error("In SET_VIEWPORT, could not find world with id '", client.roomId, "'"); - return; - } - world.setViewport(client, client.viewport); - } catch (e) { - console.error('An error occurred on "SET_VIEWPORT" event'); - console.error(e); - } - } - - private handleUserMovesMessage(client: ExSocketInterface, userMovesMessage: UserMovesMessage) { - //console.log(SockerIoEvent.USER_POSITION, userMovesMessage); - try { - const userMoves = userMovesMessage.toObject(); - - // If CPU is high, let's drop messages of users moving (we will only dispatch the final position) - if (cpuTracker.isOverHeating() && userMoves.position?.moving === true) { - return; - } - - const position = userMoves.position; - if (position === undefined) { - throw new Error('Position not found in message'); - } - const viewport = userMoves.viewport; - if (viewport === undefined) { - throw new Error('Viewport not found in message'); - } - - let direction: string; - switch (position.direction) { - case Direction.UP: - direction = 'up'; - break; - case Direction.DOWN: - direction = 'down'; - break; - case Direction.LEFT: - direction = 'left'; - break; - case Direction.RIGHT: - direction = 'right'; - break; - default: - throw new Error("Unexpected direction"); - } - - // sending to all clients in room except sender - client.position = { - x: position.x, - y: position.y, - direction, - moving: position.moving, - }; - client.viewport = viewport; - - // update position in the world - const world = this.Worlds.get(client.roomId); - if (!world) { - console.error("In USER_POSITION, could not find world with id '", client.roomId, "'"); - return; - } - world.updatePosition(client, client.position); - world.setViewport(client, client.viewport); - } catch (e) { - console.error('An error occurred on "user_position" event'); - console.error(e); - } - } - - // Useless now, will be useful again if we allow editing details in game - private handleSetPlayerDetails(client: ExSocketInterface, playerDetailsMessage: SetPlayerDetailsMessage) { - const playerDetails = { - name: playerDetailsMessage.getName(), - characterLayers: playerDetailsMessage.getCharacterlayersList() - }; - //console.log(SocketIoEvent.SET_PLAYER_DETAILS, playerDetails); - if (!isSetPlayerDetailsMessage(playerDetails)) { - this.emitError(client, 'Invalid SET_PLAYER_DETAILS message received: '); - return; - } - client.name = playerDetails.name; - client.characterLayers = playerDetails.characterLayers; - - } - - private handleSilentMessage(client: ExSocketInterface, silentMessage: SilentMessage) { - try { - // update position in the world - const world = this.Worlds.get(client.roomId); - if (!world) { - console.error("In handleSilentMessage, could not find world with id '", client.roomId, "'"); - return; - } - world.setSilent(client, silentMessage.getSilent()); - } catch (e) { - console.error('An error occurred on "handleSilentMessage"'); - console.error(e); - } - } - - private handleItemEvent(ws: ExSocketInterface, itemEventMessage: ItemEventMessage) { - const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage); - - try { - const world = this.Worlds.get(ws.roomId); - if (!world) { - console.error("Could not find world with id '", ws.roomId, "'"); - return; - } - - const subMessage = new SubMessage(); - subMessage.setItemeventmessage(itemEventMessage); - - // Let's send the event without using the SocketIO room. - for (const user of world.getUsers().values()) { - const client = this.searchClientByIdOrFail(user.id); - //client.emit(SocketIoEvent.ITEM_EVENT, itemEvent); - emitInBatch(client, subMessage); - } - - world.setItemState(itemEvent.itemId, itemEvent.state); - } catch (e) { - console.error('An error occurred on "item_event"'); - console.error(e); - } - } - - private handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) { - try { - const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); - if (!reportedSocket) { - throw 'reported socket user not found'; - } - //TODO report user on admin application - Axios.post(`${ADMIN_API_URL}/api/report`, { - reportedUserUuid: reportedSocket.userUuid, - reportedUserComment: reportPlayerMessage.getReportcomment(), - reporterUserUuid: client.userUuid - }, - { - headers: {"Authorization": `${ADMIN_API_TOKEN}`} - }).catch((err) => { - throw err; - }); - } catch (e) { - console.error('An error occurred on "handleReportMessage"'); - console.error(e); - } - } - - emitVideo(socket: ExSocketInterface, data: WebRtcSignalToServerMessage): void { - //send only at user - const client = this.sockets.get(data.getReceiverid()); - if (client === undefined) { - console.warn("While exchanging a WebRTC signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); - return; - } - - const webrtcSignalToClient = new WebRtcSignalToClientMessage(); - webrtcSignalToClient.setUserid(socket.userId); - webrtcSignalToClient.setSignal(data.getSignal()); - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient); - - if (!client.disconnecting) { - client.send(serverToClientMessage.serializeBinary().buffer, true); - } - } - - emitScreenSharing(socket: ExSocketInterface, data: WebRtcSignalToServerMessage): void { - //send only at user - const client = this.sockets.get(data.getReceiverid()); - if (client === undefined) { - console.warn("While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); - return; - } - - const webrtcSignalToClient = new WebRtcSignalToClientMessage(); - webrtcSignalToClient.setUserid(socket.userId); - webrtcSignalToClient.setSignal(data.getSignal()); - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient); - - if (!client.disconnecting) { - client.send(serverToClientMessage.serializeBinary().buffer, true); - } - } - - searchClientByIdOrFail(userId: number): ExSocketInterface { - const client: ExSocketInterface|undefined = this.sockets.get(userId); - if (client === undefined) { - throw new Error("Could not find user with id " + userId); - } + client.name = ws.name; + client.tags = ws.tags; + client.characterLayers = ws.characterLayers; + client.roomId = ws.roomId; return client; } - - leaveRoom(Client : ExSocketInterface){ - // leave previous room and world - if(Client.roomId){ - try { - //user leave previous world - const world: GameRoom | undefined = this.Worlds.get(Client.roomId); - if (world) { - world.leave(Client); - if (world.isEmpty()) { - this.Worlds.delete(Client.roomId); - } - } - //user leave previous room - //Client.leave(Client.roomId); - } finally { - this.nbClientsPerRoomGauge.dec({ room: Client.roomId }); - //delete Client.roomId; - } - } - } - - private async getOrCreateRoom(roomId: string): Promise { - //check and create new world for a room - let world = this.Worlds.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, listener: User) => this.onRoomEnter(thing, listener), - (thing: Movable, position:PositionInterface, listener:User) => this.onClientMove(thing, position, listener), - (thing: Movable, listener:User) => this.onClientLeave(thing, listener) - ); - if (!world.anonymous) { - const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug) - world.tags = data.tags - world.policyType = Number(data.policy_type) - } - this.Worlds.set(roomId, world); - } - return Promise.resolve(world) - } - - private joinRoom(client : ExSocketInterface, position: PointInterface): GameRoom { - - const roomId = client.roomId; - //join user in room - this.nbClientsPerRoomGauge.inc({ room: roomId }); - client.position = position; - - const world = this.Worlds.get(roomId) - if(world === undefined){ - throw new Error('Could not find room for ID: '+client.roomId) - } - - // Dispatch groups position to newly connected user - world.getGroups().forEach((group: Group) => { - this.emitCreateUpdateGroupEvent(client, group); - }); - //join world - world.join(client, client.position); - return world; - } - - private onRoomEnter(thing: Movable, listener: User) { - const clientListener = this.searchClientByIdOrFail(listener.id); - if (thing instanceof User) { - const clientUser = this.searchClientByIdOrFail(thing.id); - - const userJoinedMessage = new UserJoinedMessage(); - if (!Number.isInteger(clientUser.userId)) { - throw new Error('clientUser.userId is not an integer '+clientUser.userId); - } - userJoinedMessage.setUserid(clientUser.userId); - userJoinedMessage.setName(clientUser.name); - userJoinedMessage.setCharacterlayersList(clientUser.characterLayers); - userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position)); - - const subMessage = new SubMessage(); - subMessage.setUserjoinedmessage(userJoinedMessage); - - emitInBatch(clientListener, subMessage); - } else if (thing instanceof Group) { - this.emitCreateUpdateGroupEvent(clientListener, thing); - } else { - console.error('Unexpected type for Movable.'); - } - } - - private onClientMove(thing: Movable, position:PositionInterface, listener:User): void { - const clientListener = this.searchClientByIdOrFail(listener.id); - if (thing instanceof User) { - const clientUser = this.searchClientByIdOrFail(thing.id); - - const userMovedMessage = new UserMovedMessage(); - userMovedMessage.setUserid(clientUser.userId); - userMovedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position)); - - const subMessage = new SubMessage(); - subMessage.setUsermovedmessage(userMovedMessage); - - clientListener.emitInBatch(subMessage); - //console.log("Sending USER_MOVED event"); - } else if (thing instanceof Group) { - this.emitCreateUpdateGroupEvent(clientListener, thing); - } else { - console.error('Unexpected type for Movable.'); - } - } - - private onClientLeave(thing: Movable, listener:User) { - const clientListener = this.searchClientByIdOrFail(listener.id); - if (thing instanceof User) { - const clientUser = this.searchClientByIdOrFail(thing.id); - this.emitUserLeftEvent(clientListener, clientUser.userId); - } else if (thing instanceof Group) { - this.emitDeleteGroupEvent(clientListener, thing.getId()); - } else { - console.error('Unexpected type for Movable.'); - } - } - - private emitCreateUpdateGroupEvent(client: ExSocketInterface, group: Group): void { - const position = group.getPosition(); - const pointMessage = new PointMessage(); - pointMessage.setX(Math.floor(position.x)); - pointMessage.setY(Math.floor(position.y)); - const groupUpdateMessage = new GroupUpdateMessage(); - groupUpdateMessage.setGroupid(group.getId()); - groupUpdateMessage.setPosition(pointMessage); - - const subMessage = new SubMessage(); - subMessage.setGroupupdatemessage(groupUpdateMessage); - - emitInBatch(client, subMessage); - //socket.emit(SocketIoEvent.GROUP_CREATE_UPDATE, groupUpdateMessage.serializeBinary().buffer); - } - - private emitDeleteGroupEvent(client: ExSocketInterface, groupId: number): void { - const groupDeleteMessage = new GroupDeleteMessage(); - groupDeleteMessage.setGroupid(groupId); - - const subMessage = new SubMessage(); - subMessage.setGroupdeletemessage(groupDeleteMessage); - - emitInBatch(client, subMessage); - } - - private emitUserLeftEvent(client: ExSocketInterface, userId: number): void { - const userLeftMessage = new UserLeftMessage(); - userLeftMessage.setUserid(userId); - - const subMessage = new SubMessage(); - subMessage.setUserleftmessage(userLeftMessage); - - emitInBatch(client, subMessage); - } - - joinWebRtcRoom(user: User, group: Group) { - /*const roomId: string = "webrtcroom"+group.getId(); - if (user.socket.webRtcRoomId === roomId) { - return; - }*/ - - for (const otherUser of group.getUsers()) { - if (user === otherUser) { - continue; - } - - // Let's send 2 messages: one to the user joining the group and one to the other user - const webrtcStartMessage1 = new WebRtcStartMessage(); - webrtcStartMessage1.setUserid(otherUser.id); - webrtcStartMessage1.setName(otherUser.socket.name); - webrtcStartMessage1.setInitiator(true); - - const serverToClientMessage1 = new ServerToClientMessage(); - serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); - - if (!user.socket.disconnecting) { - user.socket.send(serverToClientMessage1.serializeBinary().buffer, true); - //console.log('Sending webrtcstart initiator to '+user.socket.userId) - } - - const webrtcStartMessage2 = new WebRtcStartMessage(); - webrtcStartMessage2.setUserid(user.id); - webrtcStartMessage2.setName(user.socket.name); - webrtcStartMessage2.setInitiator(false); - - const serverToClientMessage2 = new ServerToClientMessage(); - serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); - - if (!otherUser.socket.disconnecting) { - otherUser.socket.send(serverToClientMessage2.serializeBinary().buffer, true); - //console.log('Sending webrtcstart to '+otherUser.socket.userId) - } - - } - -/* socket.join(roomId); - socket.webRtcRoomId = roomId; - //if two persons in room share - if (this.Io.sockets.adapter.rooms[roomId].length < 2) { - return; - } - - // TODO: scanning all sockets is maybe not the most efficient - const clients: Array = (Object.values(this.Io.sockets.sockets) as Array) - .filter((client: ExSocketInterface) => client.webRtcRoomId && client.webRtcRoomId === roomId); - //send start at one client to initialise offer webrtc - //send all users in room to create PeerConnection in front - clients.forEach((client: ExSocketInterface, index: number) => { - - const peerClients = clients.reduce((tabs: Array, clientId: ExSocketInterface, indexClientId: number) => { - if (!clientId.userId || clientId.userId === client.userId) { - return tabs; - } - tabs.push({ - userId: clientId.userId, - name: clientId.name, - initiator: index <= indexClientId - }); - return tabs; - }, []); - - client.emit(SocketIoEvent.WEBRTC_START, {clients: peerClients, roomId: roomId}); - });*/ - } - - /** permit to share user position - ** users position will send in event 'user-position' - ** The data sent is an array with information for each user : - [ - { - userId: , - roomId: , - position: { - x : , - y : , - direction: - } - }, - ... - ] - **/ - - //disconnect user - disConnectedUser(user: User, group: Group) { - // Most of the time, sending a disconnect event to one of the players is enough (the player will close the connection - // which will be shut for the other player). - // However! In the rare case where the WebRTC connection is not yet established, if we close the connection on one of the player, - // the other player will try connecting until a timeout happens (during this time, the connection icon will be displayed for nothing). - // So we also send the disconnect event to the other player. - for (const otherUser of group.getUsers()) { - if (user === otherUser) { - continue; - } - - const webrtcDisconnectMessage1 = new WebRtcDisconnectMessage(); - webrtcDisconnectMessage1.setUserid(user.id); - - const serverToClientMessage1 = new ServerToClientMessage(); - serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1); - - if (!otherUser.socket.disconnecting) { - otherUser.socket.send(serverToClientMessage1.serializeBinary().buffer, true); - } - - - const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage(); - webrtcDisconnectMessage2.setUserid(otherUser.id); - - const serverToClientMessage2 = new ServerToClientMessage(); - serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2); - - if (!user.socket.disconnecting) { - user.socket.send(serverToClientMessage2.serializeBinary().buffer, true); - } - } - - //disconnect webrtc room - /*if(!Client.webRtcRoomId){ - return; - }*/ - //Client.leave(Client.webRtcRoomId); - //delete Client.webRtcRoomId; - } - - private emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) { - try { - const world = this.Worlds.get(client.roomId); - if (!world) { - console.error("In emitPlayGlobalMessage, could not find world with id '", client.roomId, "'"); - return; - } - - const serverToClientMessage = new ServerToClientMessage(); - serverToClientMessage.setPlayglobalmessage(playglobalmessage); - - for (const [id, user] of world.getUsers().entries()) { - user.socket.send(serverToClientMessage.serializeBinary().buffer, true); - } - } catch (e) { - console.error('An error occurred on "emitPlayGlobalMessage" event'); - console.error(e); - } - - } - - public getWorlds(): Map { - return this.Worlds; - } - - /** - * - * @param token - */ - searchClientByUuid(uuid: string): ExSocketInterface | null { - for(const socket of this.sockets.values()){ - if(socket.userUuid === uuid){ - return socket; - } - } - return null; - } } diff --git a/back/src/Controller/PrometheusController.ts b/back/src/Controller/PrometheusController.ts index 95254af8..05570466 100644 --- a/back/src/Controller/PrometheusController.ts +++ b/back/src/Controller/PrometheusController.ts @@ -5,7 +5,7 @@ const register = require('prom-client').register; const collectDefaultMetrics = require('prom-client').collectDefaultMetrics; export class PrometheusController { - constructor(private App: App, private ioSocketController: IoSocketController) { + constructor(private App: App) { collectDefaultMetrics({ timeout: 10000, gcDurationBuckets: [0.001, 0.01, 0.1, 1, 2, 5], // These are the default buckets. diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts index 739997fd..c9b40f03 100644 --- a/back/src/Services/AdminApi.ts +++ b/back/src/Services/AdminApi.ts @@ -11,11 +11,6 @@ export interface AdminApiData { userUuid: string } -export interface GrantedApiData { - granted: boolean, - memberTags: string[] -} - export interface fetchMemberDataByUuidResponse { uuid: string; tags: string[]; @@ -66,6 +61,17 @@ class AdminApi { ) return res.data; } + + reportPlayer(reportedUserUuid: string, reportedUserComment: string, reporterUserUuid: string) { + return Axios.post(`${ADMIN_API_URL}/api/report`, { + reportedUserUuid, + reportedUserComment, + reporterUserUuid, + }, + { + headers: {"Authorization": `${ADMIN_API_TOKEN}`} + }); + } } export const adminApi = new AdminApi(); diff --git a/back/src/Services/IoSocketHelpers.ts b/back/src/Services/IoSocketHelpers.ts new file mode 100644 index 00000000..2166a53e --- /dev/null +++ b/back/src/Services/IoSocketHelpers.ts @@ -0,0 +1,50 @@ +import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; +import {BatchMessage, ErrorMessage, ServerToClientMessage, SubMessage} from "../Messages/generated/messages_pb"; + +export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): void { + socket.batchedMessages.addPayload(payload); + + if (socket.batchTimeout === null) { + socket.batchTimeout = setTimeout(() => { + if (socket.disconnecting) { + return; + } + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setBatchmessage(socket.batchedMessages); + + socket.send(serverToClientMessage.serializeBinary().buffer, true); + socket.batchedMessages = new BatchMessage(); + socket.batchTimeout = null; + }, 100); + } + + // If we send a message, we don't need to keep the connection alive + resetPing(socket); +} + +export function resetPing(ws: ExSocketInterface): void { + if (ws.pingTimeout) { + clearTimeout(ws.pingTimeout); + } + ws.pingTimeout = setTimeout(() => { + if (ws.disconnecting) { + return; + } + ws.ping(); + resetPing(ws); + }, 29000); +} + +export function emitError(Client: ExSocketInterface, message: string): void { + const errorMessage = new ErrorMessage(); + errorMessage.setMessage(message); + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setErrormessage(errorMessage); + + if (!Client.disconnecting) { + Client.send(serverToClientMessage.serializeBinary().buffer, true); + } + console.warn(message); +} \ No newline at end of file diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts new file mode 100644 index 00000000..a09039ad --- /dev/null +++ b/back/src/Services/SocketManager.ts @@ -0,0 +1,598 @@ +import {GameRoom} from "../Model/GameRoom"; +import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; +import { + GroupDeleteMessage, + GroupUpdateMessage, + ItemEventMessage, + ItemStateMessage, + PlayGlobalMessage, + PointMessage, + PositionMessage, + RoomJoinedMessage, + ServerToClientMessage, + SetPlayerDetailsMessage, + SilentMessage, + SubMessage, + ReportPlayerMessage, + UserJoinedMessage, UserLeftMessage, + UserMovedMessage, + UserMovesMessage, + ViewportMessage, WebRtcDisconnectMessage, + WebRtcSignalToClientMessage, + WebRtcSignalToServerMessage, WebRtcStartMessage +} from "../Messages/generated/messages_pb"; +import {PointInterface} from "../Model/Websocket/PointInterface"; +import {User} from "../Model/User"; +import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; +import {Group} from "../Model/Group"; +import {cpuTracker} from "./CpuTracker"; +import {isSetPlayerDetailsMessage} from "../Model/Websocket/SetPlayerDetailsMessage"; +import {GROUP_RADIUS, MINIMUM_DISTANCE} from "../Enum/EnvironmentVariable"; +import {Movable} from "../Model/Movable"; +import {PositionInterface} from "../Model/PositionInterface"; +import {adminApi} from "./AdminApi"; +import Direction = PositionMessage.Direction; +import {Gauge} from "prom-client"; +import {emitError, emitInBatch} from "./IoSocketHelpers"; + +class SocketManager { + private Worlds: Map = new Map(); + private sockets: Map = new Map(); + private nbClientsGauge: Gauge; + private nbClientsPerRoomGauge: Gauge; + + constructor() { + this.nbClientsGauge = new Gauge({ + 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' ] + }); + } + + handleJoinRoom(client: ExSocketInterface): void { + const position = client.position; + const viewport = client.viewport; + try { + this.sockets.set(client.userId, client); //todo: should this be at the end of the function? + this.nbClientsGauge.inc(); + // Let's log server load when a user joins + console.log(new Date().toISOString() + ' A user joined (', socketManager.sockets.size, ' connected users)'); + + //join new previous room + const gameRoom = this.joinRoom(client, position); + + const things = gameRoom.setViewport(client, viewport); + + const roomJoinedMessage = new RoomJoinedMessage(); + + for (const thing of things) { + if (thing instanceof User) { + const player: ExSocketInterface|undefined = this.sockets.get(thing.id); + if (player === undefined) { + console.warn('Something went wrong. The World contains a user "'+thing.id+"' but this user does not exist in the sockets list!"); + continue; + } + + const userJoinedMessage = new UserJoinedMessage(); + userJoinedMessage.setUserid(thing.id); + userJoinedMessage.setName(player.name); + userJoinedMessage.setCharacterlayersList(player.characterLayers); + userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(player.position)); + + roomJoinedMessage.addUser(userJoinedMessage); + } else if (thing instanceof Group) { + const groupUpdateMessage = new GroupUpdateMessage(); + groupUpdateMessage.setGroupid(thing.getId()); + groupUpdateMessage.setPosition(ProtobufUtils.toPointMessage(thing.getPosition())); + + roomJoinedMessage.addGroup(groupUpdateMessage); + } else { + console.error("Unexpected type for Movable returned by setViewport"); + } + } + + for (const [itemId, item] of gameRoom.getItemsState().entries()) { + const itemStateMessage = new ItemStateMessage(); + itemStateMessage.setItemid(itemId); + itemStateMessage.setStatejson(JSON.stringify(item)); + + roomJoinedMessage.addItem(itemStateMessage); + } + + roomJoinedMessage.setCurrentuserid(client.userId); + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setRoomjoinedmessage(roomJoinedMessage); + + if (!client.disconnecting) { + client.send(serverToClientMessage.serializeBinary().buffer, true); + } + } catch (e) { + console.error('An error occurred on "join_room" event'); + console.error(e); + } + } + + handleViewport(client: ExSocketInterface, viewportMessage: ViewportMessage) { + try { + const viewport = viewportMessage.toObject(); + + client.viewport = viewport; + + const world = this.Worlds.get(client.roomId); + if (!world) { + console.error("In SET_VIEWPORT, could not find world with id '", client.roomId, "'"); + return; + } + world.setViewport(client, client.viewport); + } catch (e) { + console.error('An error occurred on "SET_VIEWPORT" event'); + console.error(e); + } + } + + handleUserMovesMessage(client: ExSocketInterface, userMovesMessage: UserMovesMessage) { + try { + const userMoves = userMovesMessage.toObject(); + + // If CPU is high, let's drop messages of users moving (we will only dispatch the final position) + if (cpuTracker.isOverHeating() && userMoves.position?.moving === true) { + return; + } + + const position = userMoves.position; + if (position === undefined) { + throw new Error('Position not found in message'); + } + const viewport = userMoves.viewport; + if (viewport === undefined) { + throw new Error('Viewport not found in message'); + } + + let direction: string; + switch (position.direction) { + case Direction.UP: + direction = 'up'; + break; + case Direction.DOWN: + direction = 'down'; + break; + case Direction.LEFT: + direction = 'left'; + break; + case Direction.RIGHT: + direction = 'right'; + break; + default: + throw new Error("Unexpected direction"); + } + + // sending to all clients in room except sender + client.position = { + x: position.x, + y: position.y, + direction, + moving: position.moving, + }; + client.viewport = viewport; + + // update position in the world + const world = this.Worlds.get(client.roomId); + if (!world) { + console.error("In USER_POSITION, could not find world with id '", client.roomId, "'"); + return; + } + world.updatePosition(client, client.position); + world.setViewport(client, client.viewport); + } catch (e) { + console.error('An error occurred on "user_position" event'); + console.error(e); + } + } + + // Useless now, will be useful again if we allow editing details in game + handleSetPlayerDetails(client: ExSocketInterface, playerDetailsMessage: SetPlayerDetailsMessage) { + const playerDetails = { + name: playerDetailsMessage.getName(), + characterLayers: playerDetailsMessage.getCharacterlayersList() + }; + //console.log(SocketIoEvent.SET_PLAYER_DETAILS, playerDetails); + if (!isSetPlayerDetailsMessage(playerDetails)) { + emitError(client, 'Invalid SET_PLAYER_DETAILS message received: '); + return; + } + client.name = playerDetails.name; + client.characterLayers = playerDetails.characterLayers; + + } + + handleSilentMessage(client: ExSocketInterface, silentMessage: SilentMessage) { + try { + // update position in the world + const world = this.Worlds.get(client.roomId); + if (!world) { + console.error("In handleSilentMessage, could not find world with id '", client.roomId, "'"); + return; + } + world.setSilent(client, silentMessage.getSilent()); + } catch (e) { + console.error('An error occurred on "handleSilentMessage"'); + console.error(e); + } + } + + handleItemEvent(ws: ExSocketInterface, itemEventMessage: ItemEventMessage) { + const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage); + + try { + const world = this.Worlds.get(ws.roomId); + if (!world) { + console.error("Could not find world with id '", ws.roomId, "'"); + return; + } + + const subMessage = new SubMessage(); + subMessage.setItemeventmessage(itemEventMessage); + + // Let's send the event without using the SocketIO room. + for (const user of world.getUsers().values()) { + const client = this.searchClientByIdOrFail(user.id); + //client.emit(SocketIoEvent.ITEM_EVENT, itemEvent); + emitInBatch(client, subMessage); + } + + world.setItemState(itemEvent.itemId, itemEvent.state); + } catch (e) { + console.error('An error occurred on "item_event"'); + console.error(e); + } + } + + async handleReportMessage(client: ExSocketInterface, reportPlayerMessage: ReportPlayerMessage) { + try { + const reportedSocket = this.sockets.get(reportPlayerMessage.getReporteduserid()); + if (!reportedSocket) { + throw 'reported socket user not found'; + } + //TODO report user on admin application + await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid) + } catch (e) { + console.error('An error occurred on "handleReportMessage"'); + console.error(e); + } + } + + emitVideo(socket: ExSocketInterface, data: WebRtcSignalToServerMessage): void { + //send only at user + const client = this.sockets.get(data.getReceiverid()); + if (client === undefined) { + console.warn("While exchanging a WebRTC signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); + return; + } + + const webrtcSignalToClient = new WebRtcSignalToClientMessage(); + webrtcSignalToClient.setUserid(socket.userId); + webrtcSignalToClient.setSignal(data.getSignal()); + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setWebrtcsignaltoclientmessage(webrtcSignalToClient); + + if (!client.disconnecting) { + client.send(serverToClientMessage.serializeBinary().buffer, true); + } + } + + emitScreenSharing(socket: ExSocketInterface, data: WebRtcSignalToServerMessage): void { + //send only at user + const client = this.sockets.get(data.getReceiverid()); + if (client === undefined) { + console.warn("While exchanging a WEBRTC_SCREEN_SHARING signal: client with id ", data.getReceiverid(), " does not exist. This might be a race condition."); + return; + } + + const webrtcSignalToClient = new WebRtcSignalToClientMessage(); + webrtcSignalToClient.setUserid(socket.userId); + webrtcSignalToClient.setSignal(data.getSignal()); + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setWebrtcscreensharingsignaltoclientmessage(webrtcSignalToClient); + + if (!client.disconnecting) { + client.send(serverToClientMessage.serializeBinary().buffer, true); + } + } + + private searchClientByIdOrFail(userId: number): ExSocketInterface { + const client: ExSocketInterface|undefined = this.sockets.get(userId); + if (client === undefined) { + throw new Error("Could not find user with id " + userId); + } + return client; + } + + leaveRoom(Client : ExSocketInterface){ + // leave previous room and world + if(Client.roomId){ + try { + //user leave previous world + const world: GameRoom | undefined = this.Worlds.get(Client.roomId); + if (world) { + world.leave(Client); + if (world.isEmpty()) { + this.Worlds.delete(Client.roomId); + } + } + //user leave previous room + //Client.leave(Client.roomId); + } finally { + this.nbClientsPerRoomGauge.dec({ room: Client.roomId }); + //delete Client.roomId; + this.sockets.delete(Client.userId); + // Let's log server load when a user leaves + this.nbClientsGauge.dec(); + console.log('A user left (', this.sockets.size, ' connected users)'); + } + } + } + + async getOrCreateRoom(roomId: string): Promise { + //check and create new world for a room + let world = this.Worlds.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, listener: User) => this.onRoomEnter(thing, listener), + (thing: Movable, position:PositionInterface, listener:User) => this.onClientMove(thing, position, listener), + (thing: Movable, listener:User) => this.onClientLeave(thing, listener) + ); + if (!world.anonymous) { + const data = await adminApi.fetchMapDetails(world.organizationSlug, world.worldSlug, world.roomSlug) + world.tags = data.tags + world.policyType = Number(data.policy_type) + } + this.Worlds.set(roomId, world); + } + return Promise.resolve(world) + } + + private joinRoom(client : ExSocketInterface, position: PointInterface): GameRoom { + + const roomId = client.roomId; + //join user in room + this.nbClientsPerRoomGauge.inc({ room: roomId }); + client.position = position; + + const world = this.Worlds.get(roomId) + if(world === undefined){ + throw new Error('Could not find room for ID: '+client.roomId) + } + + // Dispatch groups position to newly connected user + world.getGroups().forEach((group: Group) => { + this.emitCreateUpdateGroupEvent(client, group); + }); + //join world + world.join(client, client.position); + return world; + } + + private onRoomEnter(thing: Movable, listener: User) { + const clientListener = this.searchClientByIdOrFail(listener.id); + if (thing instanceof User) { + const clientUser = this.searchClientByIdOrFail(thing.id); + + const userJoinedMessage = new UserJoinedMessage(); + if (!Number.isInteger(clientUser.userId)) { + throw new Error('clientUser.userId is not an integer '+clientUser.userId); + } + userJoinedMessage.setUserid(clientUser.userId); + userJoinedMessage.setName(clientUser.name); + userJoinedMessage.setCharacterlayersList(clientUser.characterLayers); + userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position)); + + const subMessage = new SubMessage(); + subMessage.setUserjoinedmessage(userJoinedMessage); + + emitInBatch(clientListener, subMessage); + } else if (thing instanceof Group) { + this.emitCreateUpdateGroupEvent(clientListener, thing); + } else { + console.error('Unexpected type for Movable.'); + } + } + + private onClientMove(thing: Movable, position:PositionInterface, listener:User): void { + const clientListener = this.searchClientByIdOrFail(listener.id); + if (thing instanceof User) { + const clientUser = this.searchClientByIdOrFail(thing.id); + + const userMovedMessage = new UserMovedMessage(); + userMovedMessage.setUserid(clientUser.userId); + userMovedMessage.setPosition(ProtobufUtils.toPositionMessage(clientUser.position)); + + const subMessage = new SubMessage(); + subMessage.setUsermovedmessage(userMovedMessage); + + clientListener.emitInBatch(subMessage); + //console.log("Sending USER_MOVED event"); + } else if (thing instanceof Group) { + this.emitCreateUpdateGroupEvent(clientListener, thing); + } else { + console.error('Unexpected type for Movable.'); + } + } + + private onClientLeave(thing: Movable, listener:User) { + const clientListener = this.searchClientByIdOrFail(listener.id); + if (thing instanceof User) { + const clientUser = this.searchClientByIdOrFail(thing.id); + this.emitUserLeftEvent(clientListener, clientUser.userId); + } else if (thing instanceof Group) { + this.emitDeleteGroupEvent(clientListener, thing.getId()); + } else { + console.error('Unexpected type for Movable.'); + } + } + + private emitCreateUpdateGroupEvent(client: ExSocketInterface, group: Group): void { + const position = group.getPosition(); + const pointMessage = new PointMessage(); + pointMessage.setX(Math.floor(position.x)); + pointMessage.setY(Math.floor(position.y)); + const groupUpdateMessage = new GroupUpdateMessage(); + groupUpdateMessage.setGroupid(group.getId()); + groupUpdateMessage.setPosition(pointMessage); + + const subMessage = new SubMessage(); + subMessage.setGroupupdatemessage(groupUpdateMessage); + + emitInBatch(client, subMessage); + //socket.emit(SocketIoEvent.GROUP_CREATE_UPDATE, groupUpdateMessage.serializeBinary().buffer); + } + + private emitDeleteGroupEvent(client: ExSocketInterface, groupId: number): void { + const groupDeleteMessage = new GroupDeleteMessage(); + groupDeleteMessage.setGroupid(groupId); + + const subMessage = new SubMessage(); + subMessage.setGroupdeletemessage(groupDeleteMessage); + + emitInBatch(client, subMessage); + } + + private emitUserLeftEvent(client: ExSocketInterface, userId: number): void { + const userLeftMessage = new UserLeftMessage(); + userLeftMessage.setUserid(userId); + + const subMessage = new SubMessage(); + subMessage.setUserleftmessage(userLeftMessage); + + emitInBatch(client, subMessage); + } + + private joinWebRtcRoom(user: User, group: Group) { + /*const roomId: string = "webrtcroom"+group.getId(); + if (user.socket.webRtcRoomId === roomId) { + return; + }*/ + + for (const otherUser of group.getUsers()) { + if (user === otherUser) { + continue; + } + + // Let's send 2 messages: one to the user joining the group and one to the other user + const webrtcStartMessage1 = new WebRtcStartMessage(); + webrtcStartMessage1.setUserid(otherUser.id); + webrtcStartMessage1.setName(otherUser.socket.name); + webrtcStartMessage1.setInitiator(true); + + const serverToClientMessage1 = new ServerToClientMessage(); + serverToClientMessage1.setWebrtcstartmessage(webrtcStartMessage1); + + if (!user.socket.disconnecting) { + user.socket.send(serverToClientMessage1.serializeBinary().buffer, true); + //console.log('Sending webrtcstart initiator to '+user.socket.userId) + } + + const webrtcStartMessage2 = new WebRtcStartMessage(); + webrtcStartMessage2.setUserid(user.id); + webrtcStartMessage2.setName(user.socket.name); + webrtcStartMessage2.setInitiator(false); + + const serverToClientMessage2 = new ServerToClientMessage(); + serverToClientMessage2.setWebrtcstartmessage(webrtcStartMessage2); + + if (!otherUser.socket.disconnecting) { + otherUser.socket.send(serverToClientMessage2.serializeBinary().buffer, true); + //console.log('Sending webrtcstart to '+otherUser.socket.userId) + } + + } + } + + //disconnect user + private disConnectedUser(user: User, group: Group) { + // Most of the time, sending a disconnect event to one of the players is enough (the player will close the connection + // which will be shut for the other player). + // However! In the rare case where the WebRTC connection is not yet established, if we close the connection on one of the player, + // the other player will try connecting until a timeout happens (during this time, the connection icon will be displayed for nothing). + // So we also send the disconnect event to the other player. + for (const otherUser of group.getUsers()) { + if (user === otherUser) { + continue; + } + + const webrtcDisconnectMessage1 = new WebRtcDisconnectMessage(); + webrtcDisconnectMessage1.setUserid(user.id); + + const serverToClientMessage1 = new ServerToClientMessage(); + serverToClientMessage1.setWebrtcdisconnectmessage(webrtcDisconnectMessage1); + + if (!otherUser.socket.disconnecting) { + otherUser.socket.send(serverToClientMessage1.serializeBinary().buffer, true); + } + + + const webrtcDisconnectMessage2 = new WebRtcDisconnectMessage(); + webrtcDisconnectMessage2.setUserid(otherUser.id); + + const serverToClientMessage2 = new ServerToClientMessage(); + serverToClientMessage2.setWebrtcdisconnectmessage(webrtcDisconnectMessage2); + + if (!user.socket.disconnecting) { + user.socket.send(serverToClientMessage2.serializeBinary().buffer, true); + } + } + } + + emitPlayGlobalMessage(client: ExSocketInterface, playglobalmessage: PlayGlobalMessage) { + try { + const world = this.Worlds.get(client.roomId); + if (!world) { + console.error("In emitPlayGlobalMessage, could not find world with id '", client.roomId, "'"); + return; + } + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setPlayglobalmessage(playglobalmessage); + + for (const [id, user] of world.getUsers().entries()) { + user.socket.send(serverToClientMessage.serializeBinary().buffer, true); + } + } catch (e) { + console.error('An error occurred on "emitPlayGlobalMessage" event'); + console.error(e); + } + + } + + public getWorlds(): Map { + return this.Worlds; + } + + /** + * + * @param token + */ + searchClientByUuid(uuid: string): ExSocketInterface | null { + for(const socket of this.sockets.values()){ + if(socket.userUuid === uuid){ + return socket; + } + } + return null; + } + +} + +export const socketManager = new SocketManager(); \ No newline at end of file