diff --git a/.env.template b/.env.template index d355ab67..d0db42e3 100644 --- a/.env.template +++ b/.env.template @@ -1,3 +1,7 @@ DEBUG_MODE=false JITSI_URL=meet.jit.si +# If your Jitsi environment has authentication set up, you MUST set JITSI_PRIVATE_MODE to "true" and you MUST pass a SECRET_JITSI_KEY to generate the JWT secret +JITSI_PRIVATE_MODE=false +JITSI_ISS= +SECRET_JITSI_KEY= ADMIN_API_TOKEN=123 diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 25d2b0cd..e77fb133 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -121,6 +121,9 @@ jobs: env: KUBE_CONFIG_FILE: ${{ secrets.KUBE_CONFIG_FILE }} ADMIN_API_TOKEN: ${{ secrets.ADMIN_API_TOKEN }} + JITSI_ISS: ${{ secrets.JITSI_ISS }} + JITSI_URL: ${{ secrets.JITSI_URL }} + SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }} with: namespace: workadventure-${{ env.GITHUB_REF_SLUG }} 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..49b08bb7 100644 --- a/back/src/Controller/IoSocketController.ts +++ b/back/src/Controller/IoSocketController.ts @@ -1,113 +1,111 @@ 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, + QueryJitsiJwtMessage } 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 {adminApi, fetchMemberDataByUuidResponse} from "../Services/AdminApi"; +import {socketManager} from "../Services/SocketManager"; +import {emitInBatch, resetPing} from "../Services/IoSocketHelpers"; +import Jwt from "jsonwebtoken"; +import {clientEventsEmitter} from "../Services/ClientEventsEmitter"; +import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable"; 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', { + 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 token = query.token; + if (token !== ADMIN_API_TOKEN) { + console.log('Admin access refused for token: '+token) + res.writeStatus("401 Unauthorized").end('Incorrect token'); + } + const roomId = query.roomId as string; + res.upgrade( + {roomId}, + websocketKey, websocketProtocol, websocketExtensions, context, + ); + }, + open: (ws) => { + console.log('Admin socket connect for room: '+ws.roomId); + ws.send('Data:'+JSON.stringify(socketManager.getAdminSocketDataFor(ws.roomId as string))); + ws.clientJoinCallback = (clientUUid: string, roomId: string) => { + const wsroomId = ws.roomId as string; + if(wsroomId === roomId) { + ws.send('MemberJoin:'+clientUUid+';'+roomId); + } + }; + ws.clientLeaveCallback = (clientUUid: string, roomId: string) => { + const wsroomId = ws.roomId as string; + if(wsroomId === roomId) { + ws.send('MemberLeave:'+clientUUid+';'+roomId); + } + }; + clientEventsEmitter.registerToClientJoin(ws.clientJoinCallback); + clientEventsEmitter.registerToClientLeave(ws.clientLeaveCallback); + }, + message: (ws, arrayBuffer, isBinary): void => { + try { + //TODO refactor message type and data + 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 }); + switch (message.message.type) { + case 'ban': { + socketManager.emitSendUserMessage(messageToEmit); + break; + } + case 'banned': { + const socketUser = socketManager.emitSendUserMessage(messageToEmit); + setTimeout(() => { + socketUser.close(); + }, 10000); + break; + } + default: { + break; + } + } + } + }catch (err) { + console.error(err); + } + }, + close: (ws, code, message) => { + //todo make sure this code unregister the right listeners + clientEventsEmitter.unregisterFromClientJoin(ws.clientJoinCallback); + clientEventsEmitter.unregisterFromClientLeave(ws.clientLeaveCallback); + } + }) + } ioConnection() { this.app.ws('/room', { @@ -165,7 +163,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,56 +227,52 @@ 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); + + //get data information and shwo messages + adminApi.fetchMemberDataByUuid(client.userUuid).then((res: fetchMemberDataByUuidResponse) => { + if (!res.messages) { + return; + } + res.messages.forEach((c: unknown) => { + const messageToSend = c as { type: string, message: string }; + socketManager.emitSendUserMessage({ + userUuid: client.userUuid, + type: messageToSend.type, + message: messageToSend.message + }) + }); + }).catch((err) => { + console.error('fetchMemberDataByUuid => err', err); + }); }, message: (ws, arrayBuffer, isBinary): void => { const client = ws as ExSocketInterface; 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); + } else if (message.hasQueryjitsijwtmessage()){ + socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); } /* Ok is false if backpressure was built up, wait for drain */ @@ -292,622 +286,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/MapController.ts b/back/src/Controller/MapController.ts index 027fc5b8..abe34886 100644 --- a/back/src/Controller/MapController.ts +++ b/back/src/Controller/MapController.ts @@ -58,7 +58,7 @@ export class MapController extends BaseController{ this.addCorsHeaders(res); res.end(JSON.stringify(mapDetails)); } catch (e) { - console.error(e); + console.error(e.message || e); res.writeStatus("500 Internal Server Error") this.addCorsHeaders(res); res.end("An error occurred"); 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/Enum/EnvironmentVariable.ts b/back/src/Enum/EnvironmentVariable.ts index 61ab4cc9..0d4f5ed2 100644 --- a/back/src/Enum/EnvironmentVariable.ts +++ b/back/src/Enum/EnvironmentVariable.ts @@ -6,6 +6,9 @@ const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLER const ADMIN_API_URL = process.env.ADMIN_API_URL || 'http://admin'; 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 || ''; export { SECRET_KEY, @@ -16,4 +19,7 @@ export { GROUP_RADIUS, ALLOW_ARTILLERY, CPU_OVERHEAT_THRESHOLD, + JITSI_URL, + JITSI_ISS, + SECRET_JITSI_KEY } diff --git a/back/src/Model/GameRoom.ts b/back/src/Model/GameRoom.ts index baa54896..5efde159 100644 --- a/back/src/Model/GameRoom.ts +++ b/back/src/Model/GameRoom.ts @@ -85,7 +85,7 @@ export class GameRoom { } public join(socket : ExSocketInterface, userPosition: PointInterface): void { - const user = new User(socket.userId, userPosition, false, this.positionNotifier, socket); + const user = new User(socket.userId, socket.userUuid, userPosition, false, this.positionNotifier, socket); this.users.set(socket.userId, user); // Let's call update position to trigger the join / leave room //this.updatePosition(socket, userPosition); diff --git a/back/src/Model/User.ts b/back/src/Model/User.ts index 34377dc4..86a227f4 100644 --- a/back/src/Model/User.ts +++ b/back/src/Model/User.ts @@ -12,6 +12,7 @@ export class User implements Movable { public constructor( public id: number, + public uuid: string, private position: PointInterface, public silent: boolean, private positionNotifier: PositionNotifier, diff --git a/back/src/Services/AdminApi.ts b/back/src/Services/AdminApi.ts index 739997fd..9f51fb2e 100644 --- a/back/src/Services/AdminApi.ts +++ b/back/src/Services/AdminApi.ts @@ -9,16 +9,13 @@ export interface AdminApiData { tags: string[] policy_type: number userUuid: string -} - -export interface GrantedApiData { - granted: boolean, - memberTags: string[] + messages?: unknown[] } export interface fetchMemberDataByUuidResponse { uuid: string; tags: string[]; + messages: unknown[]; } class AdminApi { @@ -37,9 +34,9 @@ class AdminApi { params.roomSlug = roomSlug; } - const res = await Axios.get(ADMIN_API_URL+'/api/map', + const res = await Axios.get(ADMIN_API_URL + '/api/map', { - headers: {"Authorization" : `${ADMIN_API_TOKEN}`}, + headers: {"Authorization": `${ADMIN_API_TOKEN}`}, params } ) @@ -50,7 +47,7 @@ class AdminApi { if (!ADMIN_API_URL) { return Promise.reject('No admin backoffice set!'); } - const res = await Axios.get(ADMIN_API_URL+'/membership/'+uuid, + const res = await Axios.get(ADMIN_API_URL+'/api/membership/'+uuid, { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } ) return res.data; @@ -66,6 +63,28 @@ class AdminApi { ) return res.data; } + + async fetchCheckUserByToken(organizationMemberToken: string): Promise { + if (!ADMIN_API_URL) { + return Promise.reject('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}`} } + ) + 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/ClientEventsEmitter.ts b/back/src/Services/ClientEventsEmitter.ts new file mode 100644 index 00000000..7b888ef6 --- /dev/null +++ b/back/src/Services/ClientEventsEmitter.ts @@ -0,0 +1,32 @@ +const EventEmitter = require('events'); + +const clientJoinEvent = 'clientJoin'; +const clientLeaveEvent = 'clientLeave'; + +class ClientEventsEmitter extends EventEmitter { + emitClientJoin(clientUUid: string, roomId: string): void { + this.emit(clientJoinEvent, clientUUid, roomId); + } + + emitClientLeave(clientUUid: string, roomId: string): void { + this.emit(clientLeaveEvent, clientUUid, roomId); + } + + registerToClientJoin(callback: (clientUUid: string, roomId: string) => void): void { + this.on(clientJoinEvent, callback); + } + + registerToClientLeave(callback: (clientUUid: string, roomId: string) => void): void { + this.on(clientLeaveEvent, callback); + } + + unregisterFromClientJoin(callback: (clientUUid: string, roomId: string) => void): void { + this.removeListener(clientJoinEvent, callback); + } + + unregisterFromClientLeave(callback: (clientUUid: string, roomId: string) => void): void { + this.removeListener(clientLeaveEvent, callback); + } +} + +export const clientEventsEmitter = new ClientEventsEmitter(); \ No newline at end of file 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/JWTTokenManager.ts b/back/src/Services/JWTTokenManager.ts index 580140b0..f82fa001 100644 --- a/back/src/Services/JWTTokenManager.ts +++ b/back/src/Services/JWTTokenManager.ts @@ -2,6 +2,7 @@ import {ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable"; import {uuid} from "uuidv4"; import Jwt from "jsonwebtoken"; import {TokenInterface} from "../Controller/AuthenticateController"; +import {adminApi, AdminApiData} from "../Services/AdminApi"; class JWTTokenManager { @@ -32,7 +33,7 @@ class JWTTokenManager { 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)); + reject(new Error('An authentication error happened, invalid JsonWebToken. ' + err.message)); return; } if (tokenDecoded === undefined) { @@ -41,12 +42,23 @@ class JWTTokenManager { return; } + //verify token if (!this.isValidToken(tokenInterface)) { reject(new Error('Authentication error, invalid token structure.')); return; } - resolve(tokenInterface.userUuid); + //verify user in admin + 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(new Error('Authentication error, invalid token structure. ' + err)); + }); }); }); } diff --git a/back/src/Services/SocketManager.ts b/back/src/Services/SocketManager.ts new file mode 100644 index 00000000..9fd343be --- /dev/null +++ b/back/src/Services/SocketManager.ts @@ -0,0 +1,696 @@ +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, + QueryJitsiJwtMessage, + SendJitsiJwtMessage, + SendUserMessage +} 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, JITSI_ISS, MINIMUM_DISTANCE, SECRET_JITSI_KEY} 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"; +import Jwt from "jsonwebtoken"; +import {JITSI_URL} from "../Enum/EnvironmentVariable"; +import {clientEventsEmitter} from "./ClientEventsEmitter"; + +interface AdminSocketRoomsList { + [index: string]: number; +} +interface AdminSocketUsersList { + [index: string]: boolean; +} + +export interface AdminSocketData { + rooms: AdminSocketRoomsList, + users: AdminSocketUsersList, +} + +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' ] + }); + + clientEventsEmitter.registerToClientJoin((clientUUid, roomId) => { + this.nbClientsGauge.inc(); + // Let's log server load when a user joins + console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)'); + }); + clientEventsEmitter.registerToClientLeave((clientUUid, roomId) => { + this.nbClientsGauge.dec(); + // Let's log server load when a user leaves + console.log('A user left (', this.sockets.size, ' connected users)'); + }); + } + + getAdminSocketDataFor(roomId:string): AdminSocketData { + const data:AdminSocketData = { + rooms: {}, + users: {}, + } + const room = this.Worlds.get(roomId); + if (room === undefined) { + return data; + } + const users = room.getUsers(); + data.rooms[roomId] = users.size; + users.forEach(user => { + data.users[user.uuid] = true + }) + return data; + } + + 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? + clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId); + //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 { + //delete Client.roomId; + this.sockets.delete(Client.userId); + this.nbClientsPerRoomGauge.dec({ room: Client.roomId }); + clientEventsEmitter.emitClientLeave(Client.userUuid, Client.roomId); + } + } + } + + 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; + } + + + public handleQueryJitsiJwtMessage(client: ExSocketInterface, 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.'); + } + + // 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 sendJitsiJwtMessage = new SendJitsiJwtMessage(); + sendJitsiJwtMessage.setJitsiroom(room); + sendJitsiJwtMessage.setJwt(jwt); + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setSendjitsijwtmessage(sendJitsiJwtMessage); + + client.send(serverToClientMessage.serializeBinary().buffer, true); + } + + public emitSendUserMessage(messageToSend: {userUuid: string, message: string, type: string}): ExSocketInterface { + const socket = this.searchClientByUuid(messageToSend.userUuid); + if(!socket){ + throw 'socket was not found'; + } + + const sendUserMessage = new SendUserMessage(); + sendUserMessage.setMessage(messageToSend.message); + sendUserMessage.setType(messageToSend.type); + + const serverToClientMessage = new ServerToClientMessage(); + serverToClientMessage.setSendusermessage(sendUserMessage); + + if (!socket.disconnecting) { + socket.send(serverToClientMessage.serializeBinary().buffer, true); + } + return socket; + } +} + +export const socketManager = new SocketManager(); diff --git a/back/tests/PositionNotifierTest.ts b/back/tests/PositionNotifierTest.ts index 0f556866..573a3233 100644 --- a/back/tests/PositionNotifierTest.ts +++ b/back/tests/PositionNotifierTest.ts @@ -25,14 +25,14 @@ describe("PositionNotifier", () => { leaveTriggered = true; }); - const user1 = new User(1, { + const user1 = new User(1, 'test', { x: 500, y: 500, moving: false, direction: 'down' }, false, positionNotifier, {} as ExSocketInterface); - const user2 = new User(2, { + const user2 = new User(2, 'test', { x: -9999, y: -9999, moving: false, @@ -103,14 +103,14 @@ describe("PositionNotifier", () => { leaveTriggered = true; }); - const user1 = new User(1, { + const user1 = new User(1, 'test', { x: 500, y: 500, moving: false, direction: 'down' }, false, positionNotifier, {} as ExSocketInterface); - const user2 = new User(2, { + const user2 = new User(2, 'test', { x: 0, y: 0, moving: false, diff --git a/deeployer.libsonnet b/deeployer.libsonnet index df04399a..4edb4728 100644 --- a/deeployer.libsonnet +++ b/deeployer.libsonnet @@ -16,7 +16,10 @@ "env": { "SECRET_KEY": "tempSecretKeyNeedsToChange", "ADMIN_API_TOKEN": env.ADMIN_API_TOKEN, - "ADMIN_API_URL": "https://admin."+url + "ADMIN_API_URL": "https://admin."+url, + "JITSI_ISS": env.JITSI_ISS, + "JITSI_URL": env.JITSI_URL, + "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, } }, "front": { @@ -28,10 +31,12 @@ "ports": [80], "env": { "API_URL": "api."+url, - "JITSI_URL": "meet.jit.si", + "JITSI_URL": env.JITSI_URL, + "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443", "TURN_USER": "workadventure", - "TURN_PASSWORD": "WorkAdventure123" + "TURN_PASSWORD": "WorkAdventure123", + "JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false" } }, "maps": { diff --git a/docker-compose.yaml b/docker-compose.yaml index ffc846e4..482dfbcb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -23,6 +23,7 @@ services: environment: DEBUG_MODE: "$DEBUG_MODE" JITSI_URL: $JITSI_URL + JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" HOST: "0.0.0.0" NODE_ENV: development API_URL: api.workadventure.localhost @@ -72,8 +73,11 @@ services: environment: STARTUP_COMMAND_1: yarn install SECRET_KEY: yourSecretKey + SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" ALLOW_ARTILLERY: "true" ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" + JITSI_URL: $JITSI_URL + JITSI_ISS: $JITSI_ISS volumes: - ./back:/usr/src/app labels: diff --git a/front/dist/index.html b/front/dist/index.html index 5984af7b..8e957965 100644 --- a/front/dist/index.html +++ b/front/dist/index.html @@ -125,6 +125,9 @@ + diff --git a/front/dist/resources/objects/report-message.mp3 b/front/dist/resources/objects/report-message.mp3 new file mode 100644 index 00000000..0135bfaf Binary files /dev/null and b/front/dist/resources/objects/report-message.mp3 differ diff --git a/front/dist/resources/style/style.css b/front/dist/resources/style/style.css index fa91d1e7..23a4bba6 100644 --- a/front/dist/resources/style/style.css +++ b/front/dist/resources/style/style.css @@ -654,4 +654,6 @@ div.modal-report-user{ width: 100%; text-align: left; padding: 30px; + max-width: calc(800px - 60px); /* size of modal - padding*/ } + diff --git a/front/src/Administration/TypeMessage.ts b/front/src/Administration/TypeMessage.ts new file mode 100644 index 00000000..61891604 --- /dev/null +++ b/front/src/Administration/TypeMessage.ts @@ -0,0 +1,87 @@ +import {TypeMessageInterface} from "./UserMessageManager"; +import {HtmlUtils} from "../WebRtc/HtmlUtils"; + +let modalTimeOut : NodeJS.Timeout; + +export class TypeMessageExt implements TypeMessageInterface{ + private nbSecond = 0; + private maxNbSecond = 10; + private titleMessage = 'IMPORTANT !'; + + showMessage(message: string, canDeleteMessage: boolean = true): void { + //delete previous modal + try{ + if(modalTimeOut){ + clearTimeout(modalTimeOut); + } + const modal = HtmlUtils.getElementByIdOrFail('report-message-user'); + modal.remove(); + }catch (err){ + console.error(err); + } + + //create new modal + const div : HTMLDivElement = document.createElement('div'); + div.classList.add('modal-report-user'); + div.id = 'report-message-user'; + div.style.backgroundColor = '#000000e0'; + + const img : HTMLImageElement = document.createElement('img'); + img.src = 'resources/logos/report.svg'; + div.appendChild(img); + + const title : HTMLParagraphElement = document.createElement('p'); + title.id = 'title-report-user'; + title.innerText = `${this.titleMessage} (${this.maxNbSecond})`; + div.appendChild(title); + + const p : HTMLParagraphElement = document.createElement('p'); + p.id = 'body-report-user' + p.innerText = message; + div.appendChild(p); + + const mainSectionDiv = HtmlUtils.getElementByIdOrFail('main-container'); + mainSectionDiv.appendChild(div); + + const reportMessageAudio = HtmlUtils.getElementByIdOrFail('report-message'); + reportMessageAudio.play(); + + this.nbSecond = this.maxNbSecond; + setTimeout((c) => { + this.forMessage(title, canDeleteMessage); + }, 1000); + } + + forMessage(title: HTMLParagraphElement, canDeleteMessage: boolean = true){ + this.nbSecond -= 1; + title.innerText = `${this.titleMessage} (${this.nbSecond})`; + if(this.nbSecond > 0){ + modalTimeOut = setTimeout(() => { + this.forMessage(title, canDeleteMessage); + }, 1000); + }else { + title.innerText = this.titleMessage; + + if (!canDeleteMessage) { + return; + } + const imgCancel: HTMLImageElement = document.createElement('img'); + imgCancel.id = 'cancel-report-user'; + imgCancel.src = 'resources/logos/close.svg'; + + const div = HtmlUtils.getElementByIdOrFail('report-message-user'); + div.appendChild(imgCancel); + imgCancel.addEventListener('click', () => { + div.remove(); + }); + } + } +} +export class Ban extends TypeMessageExt { +} + +export class Banned extends TypeMessageExt { + showMessage(message: string){ + super.showMessage(message, false); + } +} \ No newline at end of file diff --git a/front/src/Administration/UserMessageManager.ts b/front/src/Administration/UserMessageManager.ts new file mode 100644 index 00000000..12022b03 --- /dev/null +++ b/front/src/Administration/UserMessageManager.ts @@ -0,0 +1,36 @@ +import {RoomConnection} from "../Connexion/RoomConnection"; +import * as TypeMessages from "./TypeMessage"; + +export interface TypeMessageInterface { + showMessage(message: string): void; +} + +export class UserMessageManager { + + typeMessages: Map = new Map(); + + constructor(private Connection: RoomConnection) { + const valueTypeMessageTab = Object.values(TypeMessages); + Object.keys(TypeMessages).forEach((value: string, index: number) => { + const typeMessageInstance: TypeMessageInterface = (new valueTypeMessageTab[index]() as TypeMessageInterface); + this.typeMessages.set(value.toLowerCase(), typeMessageInstance); + }); + this.initialise(); + } + + initialise() { + //receive signal to show message + this.Connection.receiveUserMessage((type: string, message: string) => { + this.showMessage(type, message); + }); + } + + showMessage(type: string, message: string) { + const classTypeMessage = this.typeMessages.get(type.toLowerCase()); + if (!classTypeMessage) { + console.error('Message unknown'); + return; + } + classTypeMessage.showMessage(message); + } +} \ No newline at end of file diff --git a/front/src/Connexion/ConnexionModels.ts b/front/src/Connexion/ConnexionModels.ts index c564ed90..fd2149c5 100644 --- a/front/src/Connexion/ConnexionModels.ts +++ b/front/src/Connexion/ConnexionModels.ts @@ -27,6 +27,8 @@ export enum EventMessage{ STOP_GLOBAL_MESSAGE = "stop-global-message", TELEPORT = "teleport", + USER_MESSAGE = "user-message", + START_JITSI_ROOM = "start-jitsi-room", } export interface PointInterface { diff --git a/front/src/Connexion/RoomConnection.ts b/front/src/Connexion/RoomConnection.ts index a9b830d3..9d04cedd 100644 --- a/front/src/Connexion/RoomConnection.ts +++ b/front/src/Connexion/RoomConnection.ts @@ -22,7 +22,7 @@ import { WebRtcSignalToServerMessage, WebRtcStartMessage, ReportPlayerMessage, - TeleportMessageMessage + TeleportMessageMessage, QueryJitsiJwtMessage, SendJitsiJwtMessage, SendUserMessage } from "../Messages/generated/messages_pb" import {UserSimplePeerInterface} from "../WebRtc/SimplePeer"; @@ -35,8 +35,6 @@ import { RoomJoinedMessageInterface, ViewportInterface, WebRtcDisconnectMessageInterface, WebRtcSignalReceivedMessageInterface, - WebRtcSignalSentMessageInterface, - WebRtcStartMessageInterface } from "./ConnexionModels"; export class RoomConnection implements RoomConnection { @@ -150,6 +148,10 @@ export class RoomConnection implements RoomConnection { this.dispatch(EventMessage.STOP_GLOBAL_MESSAGE, message.getStopglobalmessage()); } else if (message.hasTeleportmessagemessage()) { this.dispatch(EventMessage.TELEPORT, message.getTeleportmessagemessage()); + } else if (message.hasSendjitsijwtmessage()) { + this.dispatch(EventMessage.START_JITSI_ROOM, message.getSendjitsijwtmessage()); + } else if (message.hasSendusermessage()) { + this.dispatch(EventMessage.USER_MESSAGE, message.getSendusermessage()); } else { throw new Error('Unknown message received'); } @@ -477,8 +479,13 @@ export class RoomConnection implements RoomConnection { }); } + public receiveUserMessage(callback: (type: string, message: string) => void) { + return this.onMessage(EventMessage.USER_MESSAGE, (message: SendUserMessage) => { + callback(message.getType(), message.getMessage()); + }); + } + public emitGlobalMessage(message: PlayGlobalMessageInterface){ - console.log('emitGlobalMessage', message); const playGlobalMessage = new PlayGlobalMessage(); playGlobalMessage.setId(message.id); playGlobalMessage.setType(message.type); @@ -501,6 +508,25 @@ export class RoomConnection implements RoomConnection { this.socket.send(clientToServerMessage.serializeBinary().buffer); } + public emitQueryJitsiJwtMessage(jitsiRoom: string, tag: string|undefined ): void { + const queryJitsiJwtMessage = new QueryJitsiJwtMessage(); + queryJitsiJwtMessage.setJitsiroom(jitsiRoom); + if (tag !== undefined) { + queryJitsiJwtMessage.setTag(tag); + } + + const clientToServerMessage = new ClientToServerMessage(); + clientToServerMessage.setQueryjitsijwtmessage(queryJitsiJwtMessage); + + this.socket.send(clientToServerMessage.serializeBinary().buffer); + } + + public onStartJitsiRoom(callback: (jwt: string, room: string) => void): void { + this.onMessage(EventMessage.START_JITSI_ROOM, (message: SendJitsiJwtMessage) => { + callback(message.getJwt(), message.getJitsiroom()); + }); + } + public hasTag(tag: string): boolean { return this.tags.includes(tag); } diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index 0479d252..16918e06 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -4,6 +4,7 @@ const TURN_SERVER: string = process.env.TURN_SERVER || "turn:numb.viagenie.ca"; const TURN_USER: string = process.env.TURN_USER || 'g.parant@thecodingmachine.com'; const TURN_PASSWORD: string = process.env.TURN_PASSWORD || 'itcugcOHxle9Acqi$'; const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; +const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true"; const RESOLUTION = 3; const ZOOM_LEVEL = 1/*3/4*/; const POSITION_DELAY = 200; // Wait 200ms between sending position events @@ -19,5 +20,6 @@ export { TURN_SERVER, TURN_USER, TURN_PASSWORD, - JITSI_URL + JITSI_URL, + JITSI_PRIVATE_MODE } diff --git a/front/src/Phaser/Game/GameMap.ts b/front/src/Phaser/Game/GameMap.ts index a588a4e6..9f3157a0 100644 --- a/front/src/Phaser/Game/GameMap.ts +++ b/front/src/Phaser/Game/GameMap.ts @@ -1,6 +1,6 @@ import {ITiledMap} from "../Map/ITiledMap"; -export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined) => void; +export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map) => void; /** * A wrapper around a ITiledMap interface to provide additional capabilities. @@ -35,14 +35,14 @@ export class GameMap { for (const [newPropName, newPropValue] of newProps.entries()) { const oldPropValue = oldProps.get(newPropName); if (oldPropValue !== newPropValue) { - this.trigger(newPropName, oldPropValue, newPropValue); + this.trigger(newPropName, oldPropValue, newPropValue, newProps); } } for (const [oldPropName, oldPropValue] of oldProps.entries()) { if (!newProps.has(oldPropName)) { // We found a property that disappeared - this.trigger(oldPropName, oldPropValue, undefined); + this.trigger(oldPropName, oldPropValue, undefined, newProps); } } @@ -74,11 +74,11 @@ export class GameMap { return properties; } - private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined) { + private trigger(propName: string, oldValue: string | number | boolean | undefined, newValue: string | number | boolean | undefined, allProps: Map) { const callbacksArray = this.callbacks.get(propName); if (callbacksArray !== undefined) { for (const callback of callbacksArray) { - callback(newValue, oldValue); + callback(newValue, oldValue, allProps); } } } diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index 6d90525b..f82a6ce2 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -9,7 +9,14 @@ import { RoomJoinedMessageInterface } from "../../Connexion/ConnexionModels"; import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player"; -import {DEBUG_MODE, JITSI_URL, POSITION_DELAY, RESOLUTION, ZOOM_LEVEL} from "../../Enum/EnvironmentVariable"; +import { + DEBUG_MODE, + JITSI_PRIVATE_MODE, + JITSI_URL, + POSITION_DELAY, + RESOLUTION, + ZOOM_LEVEL +} from "../../Enum/EnvironmentVariable"; import { ITiledMap, ITiledMapLayer, @@ -44,6 +51,7 @@ import {ProtobufClientUtils} from "../../Network/ProtobufClientUtils"; import {connectionManager} from "../../Connexion/ConnectionManager"; import {RoomConnection} from "../../Connexion/RoomConnection"; import {GlobalMessageManager} from "../../Administration/GlobalMessageManager"; +import {UserMessageManager} from "../../Administration/UserMessageManager"; import {ConsoleGlobalMessageManager} from "../../Administration/ConsoleGlobalMessageManager"; import {ResizableScene} from "../Login/ResizableScene"; import {Room} from "../../Connexion/Room"; @@ -107,6 +115,7 @@ export class GameScene extends ResizableScene implements CenterListener { private connection!: RoomConnection; private simplePeer!: SimplePeer; private GlobalMessageManager!: GlobalMessageManager; + private UserMessageManager!: UserMessageManager; private ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager; private connectionAnswerPromise: Promise; private connectionAnswerPromiseResolve!: (value?: RoomJoinedMessageInterface | PromiseLike) => void; @@ -137,6 +146,8 @@ export class GameScene extends ResizableScene implements CenterListener { private outlinedItem: ActionableItem|null = null; private userInputManager!: UserInputManager; + private jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any + static createFromUrl(room: Room, mapUrlFile: string, gameSceneKey: string|null = null): GameScene { // We use the map URL as a key if (gameSceneKey === null) { @@ -460,34 +471,18 @@ export class GameScene extends ResizableScene implements CenterListener { CoWebsiteManager.loadCoWebsite(newValue as string); } }); - let jitsiApi: any; // eslint-disable-line @typescript-eslint/no-explicit-any - this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue) => { + this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => { if (newValue === undefined) { - this.connection.setSilent(false); - jitsiApi?.dispose(); - CoWebsiteManager.closeCoWebsite(); - mediaManager.showGameOverlay(); + this.stopJitsi(); } else { - CoWebsiteManager.insertCoWebsite((cowebsiteDiv => { - const domain = JITSI_URL; - const options = { - roomName: this.instance + "-" + newValue, - width: "100%", - height: "100%", - parentNode: cowebsiteDiv, - configOverwrite: { - prejoinPageEnabled: false - }, - interfaceConfigOverwrite: { - SHOW_CHROME_EXTENSION_BANNER: false, - MOBILE_APP_PROMO: false - } - }; - jitsiApi = new (window as any).JitsiMeetExternalAPI(domain, options); // eslint-disable-line @typescript-eslint/no-explicit-any - jitsiApi.executeCommand('displayName', gameManager.getPlayerName()); - })); - this.connection.setSilent(true); - mediaManager.hideGameOverlay(); + console.log("JITSI_PRIVATE_MODE", JITSI_PRIVATE_MODE); + if (JITSI_PRIVATE_MODE) { + const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined; + + this.connection.emitQueryJitsiJwtMessage(this.instance + "-" + newValue, adminTag); + } else { + this.startJitsi(newValue as string); + } } }) @@ -597,9 +592,17 @@ export class GameScene extends ResizableScene implements CenterListener { item.fire(message.event, message.state, message.parameters); })); + /** + * Triggered when we receive the JWT token to connect to Jitsi + */ + connection.onStartJitsiRoom((jwt, room) => { + this.startJitsi(room, jwt); + }); + // When connection is performed, let's connect SimplePeer this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic); this.GlobalMessageManager = new GlobalMessageManager(this.connection); + this.UserMessageManager = new UserMessageManager(this.connection); const self = this; this.simplePeer.registerPeerConnectionListener({ @@ -1191,4 +1194,55 @@ export class GameScene extends ResizableScene implements CenterListener { public onCenterChange(): void { this.updateCameraOffset(); } + + public startJitsi(roomName: string, jwt?: string): void { + CoWebsiteManager.insertCoWebsite((cowebsiteDiv => { + const domain = JITSI_URL; + const options = { + roomName: roomName, + jwt: jwt, + width: "100%", + height: "100%", + parentNode: cowebsiteDiv, + configOverwrite: { + prejoinPageEnabled: false + }, + interfaceConfigOverwrite: { + SHOW_CHROME_EXTENSION_BANNER: false, + MOBILE_APP_PROMO: false, + + HIDE_INVITE_MORE_HEADER: true, + + // Note: hiding brand does not seem to work, we probably need to put this on the server side. + SHOW_BRAND_WATERMARK: false, + SHOW_JITSI_WATERMARK: false, + SHOW_POWERED_BY: false, + SHOW_PROMOTIONAL_CLOSE_PAGE: false, + SHOW_WATERMARK_FOR_GUESTS: false, + + TOOLBAR_BUTTONS: [ + 'microphone', 'camera', 'closedcaptions', 'desktop', /*'embedmeeting',*/ 'fullscreen', + 'fodeviceselection', 'hangup', 'profile', 'chat', 'recording', + 'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand', + 'videoquality', 'filmstrip', /*'invite',*/ 'feedback', 'stats', 'shortcuts', + 'tileview', 'videobackgroundblur', 'download', 'help', 'mute-everyone', /*'security'*/ + ], + } + }; + if (!options.jwt) { + delete options.jwt; + } + this.jitsiApi = new (window as any).JitsiMeetExternalAPI(domain, options); // eslint-disable-line @typescript-eslint/no-explicit-any + this.jitsiApi.executeCommand('displayName', gameManager.getPlayerName()); + })); + this.connection.setSilent(true); + mediaManager.hideGameOverlay(); + } + + public stopJitsi(): void { + this.connection.setSilent(false); + this.jitsiApi?.dispose(); + CoWebsiteManager.closeCoWebsite(); + mediaManager.showGameOverlay(); + } } diff --git a/front/src/Phaser/Reconnecting/FourOFourScene.ts b/front/src/Phaser/Reconnecting/FourOFourScene.ts index 4a9b1a6f..36106796 100644 --- a/front/src/Phaser/Reconnecting/FourOFourScene.ts +++ b/front/src/Phaser/Reconnecting/FourOFourScene.ts @@ -64,13 +64,5 @@ export class FourOFourScene extends Phaser.Scene { this.cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, 'cat', 6); this.cat.flipY=true; - /*this.anims.create({ - key: 'right', - frames: this.anims.generateFrameNumbers('cat', { start: 6, end: 8 }), - frameRate: 10, - repeat: -1 - }); - cat.play('right');*/ - } } diff --git a/front/webpack.config.js b/front/webpack.config.js index cf8112ab..218b7374 100644 --- a/front/webpack.config.js +++ b/front/webpack.config.js @@ -45,7 +45,7 @@ module.exports = { new webpack.ProvidePlugin({ Phaser: 'phaser' }), - new webpack.EnvironmentPlugin(['API_URL', 'DEBUG_MODE', 'TURN_SERVER', 'TURN_USER', 'TURN_PASSWORD', 'JITSI_URL']) + new webpack.EnvironmentPlugin(['API_URL', 'DEBUG_MODE', 'TURN_SERVER', 'TURN_USER', 'TURN_PASSWORD', 'JITSI_URL', 'JITSI_PRIVATE_MODE']) ], }; diff --git a/messages/messages.proto b/messages/messages.proto index 6e00e42a..45872f22 100644 --- a/messages/messages.proto +++ b/messages/messages.proto @@ -53,6 +53,11 @@ message ReportPlayerMessage { string reportComment = 2; } +message QueryJitsiJwtMessage { + string jitsiRoom = 1; + string tag = 2; // FIXME: rather than reading the tag from the query, we should read it from the current map! +} + message ClientToServerMessage { oneof message { UserMovesMessage userMovesMessage = 2; @@ -65,6 +70,7 @@ message ClientToServerMessage { PlayGlobalMessage playGlobalMessage = 9; StopGlobalMessage stopGlobalMessage = 10; ReportPlayerMessage reportPlayerMessage = 11; + QueryJitsiJwtMessage queryJitsiJwtMessage = 12; } } @@ -167,6 +173,16 @@ message TeleportMessageMessage{ string map = 1; } +message SendJitsiJwtMessage { + string jitsiRoom = 1; + string jwt = 2; +} + +message SendUserMessage{ + string type = 1; + string message = 2; +} + message ServerToClientMessage { oneof message { BatchMessage batchMessage = 1; @@ -179,5 +195,7 @@ message ServerToClientMessage { PlayGlobalMessage playGlobalMessage = 8; StopGlobalMessage stopGlobalMessage = 9; TeleportMessageMessage teleportMessageMessage = 10; + SendJitsiJwtMessage sendJitsiJwtMessage = 11; + SendUserMessage sendUserMessage = 12; } } diff --git a/website/dist/choose-map.html b/website/dist/choose-map.html index 7d00d885..a2366f1d 100644 --- a/website/dist/choose-map.html +++ b/website/dist/choose-map.html @@ -81,7 +81,7 @@
-
+

Need a bigger Office? Visit us !