diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index e81e847b..5ae348d1 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -153,6 +153,10 @@ jobs: needs: - build-front - build-back + - build-pusher + - build-maps + - build-uploader + - build-website runs-on: ubuntu-latest steps: diff --git a/uploader/package.json b/uploader/package.json index f782c917..27e1d146 100644 --- a/uploader/package.json +++ b/uploader/package.json @@ -44,11 +44,13 @@ "iterall": "^1.3.0", "jsonwebtoken": "^8.5.1", "multer": "^1.4.2", + "mkdirp": "^1.0.4", "prom-client": "^12.0.0", "query-string": "^6.13.3", "ts-node-dev": "^1.0.0-pre.44", "typescript": "^3.8.3", - "uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0" + "uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0", + "uuidv4": "^6.0.7" }, "devDependencies": { "@types/busboy": "^0.2.3", @@ -59,6 +61,8 @@ "@types/jsonwebtoken": "^8.3.8", "@typescript-eslint/eslint-plugin": "^2.26.0", "@typescript-eslint/parser": "^2.26.0", + "@types/mkdirp": "^1.0.1", + "@types/uuidv4": "^5.0.0", "eslint": "^6.8.0", "jasmine": "^3.5.0" } diff --git a/uploader/src/Services/AdminApi.ts b/uploader/src/Services/AdminApi.ts deleted file mode 100644 index 9c46a41b..00000000 --- a/uploader/src/Services/AdminApi.ts +++ /dev/null @@ -1,115 +0,0 @@ -import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable"; -import Axios from "axios"; -import {v4} from "uuid"; - -export interface AdminApiData { - organizationSlug: string - worldSlug: string - roomSlug: string - mapUrlStart: string - tags: string[] - policy_type: number - userUuid: string - messages?: unknown[], - textures: CharacterTexture[] -} - -export interface CharacterTexture { - id: number, - level: number, - url: string, - rights: string -} - -export interface FetchMemberDataByUuidResponse { - uuid: string; - tags: string[]; - textures: CharacterTexture[]; - messages: unknown[]; -} - -class AdminApi { - - async fetchMapDetails(organizationSlug: string, worldSlug: string, roomSlug: string|undefined): Promise { - if (!ADMIN_API_URL) { - return Promise.reject('No admin backoffice set!'); - } - - const params: { organizationSlug: string, worldSlug: string, roomSlug?: string } = { - organizationSlug, - worldSlug - }; - - if (roomSlug) { - params.roomSlug = roomSlug; - } - - const res = await Axios.get(ADMIN_API_URL + '/api/map', - { - headers: {"Authorization": `${ADMIN_API_TOKEN}`}, - params - } - ) - return res.data; - } - - async fetchMemberDataByUuid(uuid: string): Promise { - if (!ADMIN_API_URL) { - return Promise.reject('No admin backoffice set!'); - } - try { - const res = await Axios.get(ADMIN_API_URL+'/api/membership/'+uuid, - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) - return res.data; - } catch (e) { - if (e?.response?.status == 404) { - // If we get an HTTP 404, the token is invalid. Let's perform an anonymous login! - console.warn('Cannot find user with uuid "'+uuid+'". Performing an anonymous login instead.'); - return { - uuid: v4(), - tags: [], - textures: [], - messages: [], - } - } else { - throw e; - } - } - } - - async fetchMemberDataByToken(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/login-url/'+organizationMemberToken, - { headers: {"Authorization" : `${ADMIN_API_TOKEN}`} } - ) - 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/uploader/src/Services/ApiClientRepository.ts b/uploader/src/Services/ApiClientRepository.ts deleted file mode 100644 index be8f14ff..00000000 --- a/uploader/src/Services/ApiClientRepository.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * A class to get connections to the correct "api" server given a room name. - */ -import {RoomManagerClient} from "../Messages/generated/messages_grpc_pb"; -import grpc from 'grpc'; -import {API_URL} from "../Enum/EnvironmentVariable"; - - -class ApiClientRepository { - private roomManagerClient: RoomManagerClient|null = null; - - public async getClient(roomId: string): Promise { - if (this.roomManagerClient === null) { - this.roomManagerClient = new RoomManagerClient(API_URL, grpc.credentials.createInsecure()); - } - return Promise.resolve(this.roomManagerClient); - } -} - -const apiClientRepository = new ApiClientRepository(); - -export { apiClientRepository }; diff --git a/uploader/src/Services/ArrayHelper.ts b/uploader/src/Services/ArrayHelper.ts deleted file mode 100644 index 67321d1b..00000000 --- a/uploader/src/Services/ArrayHelper.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const arrayIntersect = (array1: string[], array2: string[]) : boolean => { - return array1.filter(value => array2.includes(value)).length > 0; -} \ No newline at end of file diff --git a/uploader/src/Services/ClientEventsEmitter.ts b/uploader/src/Services/ClientEventsEmitter.ts deleted file mode 100644 index 7b888ef6..00000000 --- a/uploader/src/Services/ClientEventsEmitter.ts +++ /dev/null @@ -1,32 +0,0 @@ -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/uploader/src/Services/CpuTracker.ts b/uploader/src/Services/CpuTracker.ts deleted file mode 100644 index c7d57f3d..00000000 --- a/uploader/src/Services/CpuTracker.ts +++ /dev/null @@ -1,55 +0,0 @@ -import {CPU_OVERHEAT_THRESHOLD} from "../Enum/EnvironmentVariable"; - -function secNSec2ms(secNSec: Array|number) { - if (Array.isArray(secNSec)) { - return secNSec[0] * 1000 + secNSec[1] / 1000000; - } - return secNSec / 1000; -} - -class CpuTracker { - private cpuPercent: number = 0; - private overHeating: boolean = false; - - constructor() { - let time = process.hrtime.bigint() - let usage = process.cpuUsage() - setInterval(() => { - const elapTime = process.hrtime.bigint(); - const elapUsage = process.cpuUsage(usage) - usage = process.cpuUsage() - - const elapTimeMS = elapTime - time; - const elapUserMS = secNSec2ms(elapUsage.user) - const elapSystMS = secNSec2ms(elapUsage.system) - this.cpuPercent = Math.round(100 * (elapUserMS + elapSystMS) / Number(elapTimeMS) * 1000000) - - time = elapTime; - - if (!this.overHeating && this.cpuPercent > CPU_OVERHEAT_THRESHOLD) { - this.overHeating = true; - console.warn('CPU high threshold alert. Going in "overheat" mode'); - } else if (this.overHeating && this.cpuPercent <= CPU_OVERHEAT_THRESHOLD) { - this.overHeating = false; - console.log('CPU is back to normal. Canceling "overheat" mode'); - } - - /*console.log('elapsed time ms: ', elapTimeMS) - console.log('elapsed user ms: ', elapUserMS) - console.log('elapsed system ms:', elapSystMS) - console.log('cpu percent: ', this.cpuPercent)*/ - }, 100); - } - - public getCpuPercent(): number { - return this.cpuPercent; - } - - public isOverHeating(): boolean { - return this.overHeating; - } -} - -const cpuTracker = new CpuTracker(); - -export { cpuTracker }; diff --git a/uploader/src/Services/GaugeManager.ts b/uploader/src/Services/GaugeManager.ts deleted file mode 100644 index f8af822b..00000000 --- a/uploader/src/Services/GaugeManager.ts +++ /dev/null @@ -1,54 +0,0 @@ -import {Counter, Gauge} from "prom-client"; - -//this class should manage all the custom metrics used by prometheus -class GaugeManager { - private nbClientsGauge: Gauge; - private nbClientsPerRoomGauge: Gauge; - private nbGroupsPerRoomGauge: Gauge; - private nbGroupsPerRoomCounter: Counter; - - 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' ] - }); - - this.nbGroupsPerRoomCounter = new Counter({ - name: 'workadventure_counter_groups_per_room', - help: 'Counter of groups per room', - labelNames: [ 'room' ] - }); - this.nbGroupsPerRoomGauge = new Gauge({ - name: 'workadventure_nb_groups_per_room', - help: 'Number of groups per room', - labelNames: [ 'room' ] - }); - } - - incNbClientPerRoomGauge(roomId: string): void { - this.nbClientsGauge.inc(); - this.nbClientsPerRoomGauge.inc({ room: roomId }); - } - - decNbClientPerRoomGauge(roomId: string): void { - this.nbClientsGauge.dec(); - this.nbClientsPerRoomGauge.dec({ room: roomId }); - } - - incNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomCounter.inc({ room: roomId }) - this.nbGroupsPerRoomGauge.inc({ room: roomId }) - } - - decNbGroupsPerRoomGauge(roomId: string): void { - this.nbGroupsPerRoomGauge.dec({ room: roomId }) - } -} - -export const gaugeManager = new GaugeManager(); \ No newline at end of file diff --git a/uploader/src/Services/IoSocketHelpers.ts b/uploader/src/Services/IoSocketHelpers.ts deleted file mode 100644 index 9c27c59a..00000000 --- a/uploader/src/Services/IoSocketHelpers.ts +++ /dev/null @@ -1,35 +0,0 @@ -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); - } -} - -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); -} - diff --git a/uploader/src/Services/JWTTokenManager.ts b/uploader/src/Services/JWTTokenManager.ts deleted file mode 100644 index 8abb0e45..00000000 --- a/uploader/src/Services/JWTTokenManager.ts +++ /dev/null @@ -1,76 +0,0 @@ -import {ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable"; -import {uuid} from "uuidv4"; -import Jwt from "jsonwebtoken"; -import {TokenInterface} from "../Controller/AuthenticateController"; -import {adminApi, AdminApiData} from "../Services/AdminApi"; - -class JWTTokenManager { - - public createJWTToken(userUuid: string) { - return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '200d'}); //todo: add a mechanic to refresh or recreate token - } - - public async getUserUuidFromToken(token: unknown): Promise { - - if (!token) { - throw new Error('An authentication error happened, a user tried to connect without a token.'); - } - if (typeof(token) !== "string") { - throw new Error('Token is expected to be a string'); - } - - - if(token === 'test') { - if (ALLOW_ARTILLERY) { - return uuid(); - } else { - throw new Error("In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'"); - } - } - - return new Promise((resolve, reject) => { - Jwt.verify(token, SECRET_KEY, {},(err, tokenDecoded) => { - const tokenInterface = tokenDecoded as TokenInterface; - if (err) { - console.error('An authentication error happened, invalid JsonWebToken.', err); - reject(new Error('An authentication error happened, invalid JsonWebToken. ' + err.message)); - return; - } - if (tokenDecoded === undefined) { - console.error('Empty token found.'); - reject(new Error('Empty token found.')); - return; - } - - //verify token - if (!this.isValidToken(tokenInterface)) { - reject(new Error('Authentication error, invalid token structure.')); - return; - } - - if (ADMIN_API_URL) { - //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(err); - }); - } else { - resolve(tokenInterface.userUuid); - } - }); - }); - } - - private isValidToken(token: object): token is TokenInterface { - return !(typeof((token as TokenInterface).userUuid) !== 'string'); - } - -} - -export const jwtTokenManager = new JWTTokenManager(); diff --git a/uploader/src/Services/SocketManager.ts b/uploader/src/Services/SocketManager.ts deleted file mode 100644 index 2fb6c97c..00000000 --- a/uploader/src/Services/SocketManager.ts +++ /dev/null @@ -1,747 +0,0 @@ -import {PusherRoom} from "../Model/PusherRoom"; -import {CharacterLayer, 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, JoinRoomMessage, CharacterLayerMessage, PusherToBackMessage -} 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 {GROUP_RADIUS, JITSI_ISS, MINIMUM_DISTANCE, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable"; -import {Movable} from "../Model/Movable"; -import {PositionInterface} from "../Model/PositionInterface"; -import {adminApi, CharacterTexture} from "./AdminApi"; -import Direction = PositionMessage.Direction; -import {emitError, emitInBatch} from "./IoSocketHelpers"; -import Jwt from "jsonwebtoken"; -import {JITSI_URL} from "../Enum/EnvironmentVariable"; -import {clientEventsEmitter} from "./ClientEventsEmitter"; -import {gaugeManager} from "./GaugeManager"; -import {apiClientRepository} from "./ApiClientRepository"; -import {ServiceError} from "grpc"; -import {GroupDescriptor, UserDescriptor, ZoneEventListener} from "_Model/Zone"; -import Debug from "debug"; - -const debug = Debug('socket'); - -interface AdminSocketRoomsList { - [index: string]: number; -} -interface AdminSocketUsersList { - [index: string]: boolean; -} - -export interface AdminSocketData { - rooms: AdminSocketRoomsList, - users: AdminSocketUsersList, -} - -export class SocketManager implements ZoneEventListener { - private Worlds: Map = new Map(); - private sockets: Map = new Map(); - - constructor() { - clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => { - gaugeManager.incNbClientPerRoomGauge(roomId); - }); - clientEventsEmitter.registerToClientLeave((clientUUid: string, roomId: string) => { - gaugeManager.decNbClientPerRoomGauge(roomId); - }); - } - - getAdminSocketDataFor(roomId:string): AdminSocketData { - throw new Error('Not reimplemented yet'); - /*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;*/ - } - - async handleJoinRoom(client: ExSocketInterface): Promise { - const position = client.position; - const viewport = client.viewport; - try { - - const joinRoomMessage = new JoinRoomMessage(); - joinRoomMessage.setUseruuid(client.userUuid); - joinRoomMessage.setRoomid(client.roomId); - joinRoomMessage.setName(client.name); - joinRoomMessage.setPositionmessage(ProtobufUtils.toPositionMessage(client.position)); - for (const characterLayer of client.characterLayers) { - const characterLayerMessage = new CharacterLayerMessage(); - characterLayerMessage.setName(characterLayer.name); - if (characterLayer.url !== undefined) { - characterLayerMessage.setUrl(characterLayer.url); - } - - joinRoomMessage.addCharacterlayer(characterLayerMessage); - } - - - console.log('Calling joinRoom') - const apiClient = await apiClientRepository.getClient(client.roomId); - const streamToPusher = apiClient.joinRoom(); - - client.backConnection = streamToPusher; - - streamToPusher.on('data', (message: ServerToClientMessage) => { - if (message.hasRoomjoinedmessage()) { - client.userId = (message.getRoomjoinedmessage() as RoomJoinedMessage).getCurrentuserid(); - // TODO: do we need this.sockets anymore? - this.sockets.set(client.userId, client); - - // If this is the first message sent, send back the viewport. - this.handleViewport(client, viewport); - } - - // Let's pass data over from the back to the client. - if (!client.disconnecting) { - client.send(message.serializeBinary().buffer, true); - } - }).on('end', () => { - console.warn('Connection lost to back server'); - // Let's close the front connection if the back connection is closed. This way, we can retry connecting from the start. - if (!client.disconnecting) { - this.closeWebsocketConnection(client, 1011, 'Connection lost to back server'); - } - console.log('A user left'); - }).on('error', (err: Error) => { - console.error('Error in connection to back server:', err); - if (!client.disconnecting) { - this.closeWebsocketConnection(client, 1011, 'Error while connecting to back server'); - } - }); - - const pusherToBackMessage = new PusherToBackMessage(); - pusherToBackMessage.setJoinroommessage(joinRoomMessage); - streamToPusher.write(pusherToBackMessage); - - // TODO: analyze viewport, subscribe to correct handler - - //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(ProtobufUtils.toCharacterLayerMessages(player.characterLayers)); - userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(player.position)); - - roomJoinedMessage.addUser(userJoinedMessage); - roomJoinedMessage.setTagList(client.tags); - } 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 closeWebsocketConnection(client: ExSocketInterface, code: number, reason: string) { - client.disconnecting = true; - //this.leaveRoom(client); - //client.close(); - client.end(code, reason); - } - - handleViewport(client: ExSocketInterface, viewport: ViewportMessage.AsObject) { - try { - 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) { - const pusherToBackMessage = new PusherToBackMessage(); - pusherToBackMessage.setUsermovesmessage(userMovesMessage); - - client.backConnection.write(pusherToBackMessage); - - const viewport = userMovesMessage.getViewport(); - if (viewport === undefined) { - throw new Error('Missing viewport in UserMovesMessage'); - } - - // Now, we need to listen to the correct viewport. - this.handleViewport(client, viewport.toObject()) - } - - // Useless now, will be useful again if we allow editing details in game - handleSetPlayerDetails(client: ExSocketInterface, playerDetailsMessage: SetPlayerDetailsMessage) { - const pusherToBackMessage = new PusherToBackMessage(); - pusherToBackMessage.setSetplayerdetailsmessage(playerDetailsMessage); - - client.backConnection.write(pusherToBackMessage); - } - - handleSilentMessage(client: ExSocketInterface, silentMessage: SilentMessage) { - const pusherToBackMessage = new PusherToBackMessage(); - pusherToBackMessage.setSilentmessage(silentMessage); - - client.backConnection.write(pusherToBackMessage); - } - - handleItemEvent(client: ExSocketInterface, itemEventMessage: ItemEventMessage) { - const pusherToBackMessage = new PusherToBackMessage(); - pusherToBackMessage.setItemeventmessage(itemEventMessage); - - client.backConnection.write(pusherToBackMessage); - - /*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 { - const pusherToBackMessage = new PusherToBackMessage(); - pusherToBackMessage.setWebrtcsignaltoservermessage(data); - - socket.backConnection.write(pusherToBackMessage); - - - //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 { - const pusherToBackMessage = new PusherToBackMessage(); - pusherToBackMessage.setWebrtcscreensharingsignaltoservermessage(data); - - socket.backConnection.write(pusherToBackMessage); - - //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(socket : ExSocketInterface) { - // leave previous room and world - try { - if (socket.roomId) { - try { - //user leaves room - const room: PusherRoom | undefined = this.Worlds.get(socket.roomId); - if (room) { - debug('Leaving room %s.', socket.roomId); - room.leave(socket); - if (room.isEmpty()) { - this.Worlds.delete(socket.roomId); - debug('Room %s is empty. Deleting.', socket.roomId); - } - } else { - console.error('Could not find the GameRoom the user is leaving!'); - } - //user leave previous room - //Client.leave(Client.roomId); - } finally { - //delete Client.roomId; - this.sockets.delete(socket.userId); - clientEventsEmitter.emitClientLeave(socket.userUuid, socket.roomId); - console.log('A user left (', this.sockets.size, ' connected users)'); - } - } - } finally { - if (socket.backConnection) { - socket.backConnection.end(); - } - } - } - - async getOrCreateRoom(roomId: string): Promise { - //check and create new world for a room - let world = this.Worlds.get(roomId) - if(world === undefined){ - world = new PusherRoom( - roomId, - this -/* (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): PusherRoom { - - const roomId = client.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); - clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId); - console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)'); - return world; - } - - 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); - groupUpdateMessage.setGroupsize(group.getSize); - - 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) { - const pusherToBackMessage = new PusherToBackMessage(); - pusherToBackMessage.setPlayglobalmessage(playglobalmessage); - - client.backConnection.write(pusherToBackMessage); - } - - 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; - } - - /** - * Merges the characterLayers received from the front (as an array of string) with the custom textures from the back. - */ - static mergeCharacterLayersAndCustomTextures(characterLayers: string[], memberTextures: CharacterTexture[]): CharacterLayer[] { - const characterLayerObjs: CharacterLayer[] = []; - for (const characterLayer of characterLayers) { - if (characterLayer.startsWith('customCharacterTexture')) { - const customCharacterLayerId: number = +characterLayer.substr(22); - for (const memberTexture of memberTextures) { - if (memberTexture.id == customCharacterLayerId) { - characterLayerObjs.push({ - name: characterLayer, - url: memberTexture.url - }) - break; - } - } - } else { - characterLayerObjs.push({ - name: characterLayer, - url: undefined - }) - } - } - return characterLayerObjs; - } - - public onUserEnters(user: UserDescriptor, listener: ExSocketInterface): void { - const subMessage = new SubMessage(); - subMessage.setUserjoinedmessage(user.toUserJoinedMessage()); - - emitInBatch(listener, subMessage); - } - - public onUserMoves(user: UserDescriptor, listener: ExSocketInterface): void { - const subMessage = new SubMessage(); - subMessage.setUsermovedmessage(user.toUserMovedMessage()); - - emitInBatch(listener, subMessage); - } - - public onUserLeaves(userId: number, listener: ExSocketInterface): void { - const userLeftMessage = new UserLeftMessage(); - userLeftMessage.setUserid(userId); - - const subMessage = new SubMessage(); - subMessage.setUserleftmessage(userLeftMessage); - - emitInBatch(listener, subMessage); - } - - public onGroupEnters(group: GroupDescriptor, listener: ExSocketInterface): void { - const subMessage = new SubMessage(); - subMessage.setGroupupdatemessage(group.toGroupUpdateMessage()); - - emitInBatch(listener, subMessage); - } - - public onGroupMoves(group: GroupDescriptor, listener: ExSocketInterface): void { - this.onGroupEnters(group, listener); - } - - public onGroupLeaves(groupId: number, listener: ExSocketInterface): void { - const groupDeleteMessage = new GroupDeleteMessage(); - groupDeleteMessage.setGroupid(groupId); - - const subMessage = new SubMessage(); - subMessage.setGroupdeletemessage(groupDeleteMessage); - - emitInBatch(listener, subMessage); - } -} - -export const socketManager = new SocketManager(); diff --git a/uploader/yarn.lock b/uploader/yarn.lock index cc517a4b..1591fa3e 100644 --- a/uploader/yarn.lock +++ b/uploader/yarn.lock @@ -69,6 +69,13 @@ dependencies: "@types/node" "*" +"@types/mkdirp@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-1.0.1.tgz#0930b948914a78587de35458b86c907b6e98bbf6" + integrity sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q== + dependencies: + "@types/node" "*" + "@types/node@*": version "14.14.11" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.11.tgz#fc25a4248a5e8d0837019b1d170146d07334abe0" @@ -84,6 +91,18 @@ resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== +"@types/uuid@8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f" + integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== + +"@types/uuidv4@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@types/uuidv4/-/uuidv4-5.0.0.tgz#2c94e67b0c06d5adb28fb7ced1a1b5f0866ecd50" + integrity sha512-xUrhYSJnkTq9CP79cU3svoKTLPCIbMMnu9Twf/tMpHATYSHCAAeDNeb2a/29YORhk5p4atHhCTMsIBU/tvdh6A== + dependencies: + uuidv4 "*" + "@typescript-eslint/eslint-plugin@^2.26.0": version "2.34.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9" @@ -1893,6 +1912,19 @@ util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= +uuid@8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +uuidv4@*, uuidv4@^6.0.7: + version "6.2.6" + resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-6.2.6.tgz#c37c764b578114b60bdd5460e5578d7d99383ad1" + integrity sha512-vFyL4jugB/ln1ux1gXLlBMBv424Dn86EaBMoqUH1K6XI3XuriaWLeRUzH4iWwPu+BOJiw4hc4TjvrPmk+H+ZBQ== + dependencies: + "@types/uuid" "8.3.0" + uuid "8.3.2" + v8-compile-cache@^2.0.3: version "2.2.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132"