Merge pull request #347 from thecodingmachine/develop
Release 2020/10/20
This commit is contained in:
commit
479bcc2f4d
@ -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
|
||||
|
3
.github/workflows/build-and-deploy.yml
vendored
3
.github/workflows/build-and-deploy.yml
vendored
@ -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 }}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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<string, GameRoom> = new Map<string, GameRoom>();
|
||||
private sockets: Map<number, ExSocketInterface> = new Map<number, ExSocketInterface>();
|
||||
private nbClientsGauge: Gauge<string>;
|
||||
private nbClientsPerRoomGauge: Gauge<string>;
|
||||
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<GameRoom> {
|
||||
//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<ExSocketInterface> = (Object.values(this.Io.sockets.sockets) as Array<ExSocketInterface>)
|
||||
.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<UserInGroupInterface>, 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: <string>,
|
||||
roomId: <string>,
|
||||
position: {
|
||||
x : <number>,
|
||||
y : <number>,
|
||||
direction: <string>
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
**/
|
||||
|
||||
//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<string, GameRoom> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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<AdminApiData> {
|
||||
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();
|
||||
|
32
back/src/Services/ClientEventsEmitter.ts
Normal file
32
back/src/Services/ClientEventsEmitter.ts
Normal file
@ -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();
|
50
back/src/Services/IoSocketHelpers.ts
Normal file
50
back/src/Services/IoSocketHelpers.ts
Normal file
@ -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);
|
||||
}
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
696
back/src/Services/SocketManager.ts
Normal file
696
back/src/Services/SocketManager.ts
Normal file
@ -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<string, GameRoom> = new Map<string, GameRoom>();
|
||||
private sockets: Map<number, ExSocketInterface> = new Map<number, ExSocketInterface>();
|
||||
private nbClientsGauge: Gauge<string>;
|
||||
private nbClientsPerRoomGauge: Gauge<string>;
|
||||
|
||||
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<GameRoom> {
|
||||
//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<string, GameRoom> {
|
||||
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();
|
@ -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,
|
||||
|
@ -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": {
|
||||
|
@ -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:
|
||||
|
3
front/dist/index.html
vendored
3
front/dist/index.html
vendored
@ -125,6 +125,9 @@
|
||||
<audio id="audio-webrtc-in">
|
||||
<source src="/resources/objects/webrtc-in.mp3" type="audio/mp3">
|
||||
</audio>
|
||||
<audio id="report-message">
|
||||
<source src="/resources/objects/report-message.mp3" type="audio/mp3">
|
||||
</audio>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
BIN
front/dist/resources/objects/report-message.mp3
vendored
Normal file
BIN
front/dist/resources/objects/report-message.mp3
vendored
Normal file
Binary file not shown.
2
front/dist/resources/style/style.css
vendored
2
front/dist/resources/style/style.css
vendored
@ -654,4 +654,6 @@ div.modal-report-user{
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 30px;
|
||||
max-width: calc(800px - 60px); /* size of modal - padding*/
|
||||
}
|
||||
|
||||
|
87
front/src/Administration/TypeMessage.ts
Normal file
87
front/src/Administration/TypeMessage.ts
Normal file
@ -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<HTMLDivElement>('main-container');
|
||||
mainSectionDiv.appendChild(div);
|
||||
|
||||
const reportMessageAudio = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('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<HTMLDivElement>('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);
|
||||
}
|
||||
}
|
36
front/src/Administration/UserMessageManager.ts
Normal file
36
front/src/Administration/UserMessageManager.ts
Normal file
@ -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<string, TypeMessageInterface> = new Map<string, TypeMessageInterface>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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<string, string | boolean | number>) => 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<string, string | boolean | number>) {
|
||||
const callbacksArray = this.callbacks.get(propName);
|
||||
if (callbacksArray !== undefined) {
|
||||
for (const callback of callbacksArray) {
|
||||
callback(newValue, oldValue);
|
||||
callback(newValue, oldValue, allProps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<RoomJoinedMessageInterface>;
|
||||
private connectionAnswerPromiseResolve!: (value?: RoomJoinedMessageInterface | PromiseLike<RoomJoinedMessageInterface>) => 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();
|
||||
}
|
||||
}
|
||||
|
@ -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');*/
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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'])
|
||||
],
|
||||
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
2
website/dist/choose-map.html
vendored
2
website/dist/choose-map.html
vendored
@ -81,7 +81,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-sm-6 col-md-4">
|
||||
<div class="map-item" data-url="api.workadventu.re/map/files/Floor0/floor0.json" id="map_7">
|
||||
<div class="map-item" data-url="maps.workadventu.re/Floor0/floor0.json" id="map_7">
|
||||
<img src="static/images/maps/tcm.png">
|
||||
<p>Need a bigger Office? Visit us !</p>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user