Added support for quiet areas
[libreadventure.git] / back / src / Controller / IoSocketController.ts
CommitLineData
ba335aa3 1import socketIO = require('socket.io');
2import {Socket} from "socket.io";
3import * as http from "http";
cdfa9acf 4import {MessageUserPosition, Point} from "../Model/Websocket/MessageUserPosition"; //TODO fix import by "_Model/.."
53e1600e 5import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
ba47d8b1 6import Jwt, {JsonWebTokenError} from "jsonwebtoken";
3b27f8b0 7import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
881bb04e 8import {World} from "../Model/World";
e06b20fe 9import {Group} from "_Model/Group";
02e6b50b 10import {UserInterface} from "_Model/UserInterface";
b80e3e07 11import {SetPlayerDetailsMessage} from "_Model/Websocket/SetPlayerDetailsMessage";
125a4d11
DN
12import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined";
13import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved";
66ec1117 14import si from "systeminformation";
2bfa57b0 15
16enum SockerIoEvent {
17 CONNECTION = "connection",
fdb40ec3 18 DISCONNECT = "disconnect",
125a4d11
DN
19 JOIN_ROOM = "join-room", // bi-directional
20 USER_POSITION = "user-position", // bi-directional
21 USER_MOVED = "user-moved", // From server to client
22 USER_LEFT = "user-left", // From server to client
2bfa57b0 23 WEBRTC_SIGNAL = "webrtc-signal",
fdb40ec3 24 WEBRTC_OFFER = "webrtc-offer",
2bfa57b0 25 WEBRTC_START = "webrtc-start",
fdb40ec3 26 WEBRTC_DISCONNECT = "webrtc-disconect",
2bfa57b0 27 MESSAGE_ERROR = "message-error",
02e6b50b
DN
28 GROUP_CREATE_UPDATE = "group-create-update",
29 GROUP_DELETE = "group-delete",
b80e3e07 30 SET_PLAYER_DETAILS = "set-player-details"
2bfa57b0 31}
ba335aa3 32
372f938b 33export class IoSocketController {
ba335aa3 34 Io: socketIO.Server;
8a91190d 35 Worlds: Map<string, World> = new Map<string, World>();
e934015d 36 sockets: Map<string, ExSocketInterface> = new Map<string, ExSocketInterface>();
372f938b 37
38 constructor(server: http.Server) {
ba335aa3 39 this.Io = socketIO(server);
ba47d8b1 40
d064aca5 41 // Authentication with token. it will be decoded and stored in the socket.
4de55243 42 // Completely commented for now, as we do not use the "/login" route at all.
ab32021f 43 this.Io.use((socket: Socket, next) => {
ba47d8b1 44 if (!socket.handshake.query || !socket.handshake.query.token) {
45 return next(new Error('Authentication error'));
46 }
ec297e39 47 if(this.searchClientByToken(socket.handshake.query.token)){
48 return next(new Error('Authentication error'));
49 }
ab32021f 50 Jwt.verify(socket.handshake.query.token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: any) => {
ba47d8b1 51 if (err) {
52 return next(new Error('Authentication error'));
53 }
54 (socket as ExSocketInterface).token = tokenDecoded;
0c9cbca7 55 (socket as ExSocketInterface).userId = tokenDecoded.userId;
ba47d8b1 56 next();
57 });
ab32021f 58 });
ba47d8b1 59
ba335aa3 60 this.ioConnection();
02e6b50b
DN
61 }
62
ab32021f
GP
63 /**
64 *
65 * @param token
66 */
67 searchClientByToken(token: string): ExSocketInterface | null {
68 let clients: Array<any> = Object.values(this.Io.sockets.sockets);
69 for (let i = 0; i < clients.length; i++) {
70 let client: ExSocketInterface = clients[i];
71 if (client.token !== token) {
72 continue
73 }
74 return client;
75 }
76 return null;
77 }
78
02e6b50b
DN
79 private sendUpdateGroupEvent(group: Group): void {
80 // Let's get the room of the group. To do this, let's get anyone in the group and find its room.
81 // Note: this is suboptimal
82 let userId = group.getUsers()[0].id;
e934015d 83 let client: ExSocketInterface = this.searchClientByIdOrFail(userId);
02e6b50b
DN
84 let roomId = client.roomId;
85 this.Io.in(roomId).emit(SockerIoEvent.GROUP_CREATE_UPDATE, {
86 position: group.getPosition(),
87 groupId: group.getId()
88 });
89 }
90
91 private sendDeleteGroupEvent(uuid: string, lastUser: UserInterface): void {
92 // Let's get the room of the group. To do this, let's get anyone in the group and find its room.
02e6b50b 93 let userId = lastUser.id;
e934015d 94 let client: ExSocketInterface = this.searchClientByIdOrFail(userId);
02e6b50b
DN
95 let roomId = client.roomId;
96 this.Io.in(roomId).emit(SockerIoEvent.GROUP_DELETE, uuid);
ba335aa3 97 }
98
99 ioConnection() {
372f938b 100 this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => {
0c9cbca7
GP
101 let client : ExSocketInterface = socket as ExSocketInterface;
102 this.sockets.set(client.userId, client);
66ec1117
DN
103
104 // Let's log server load when a user joins
105 let srvSockets = this.Io.sockets.sockets;
69cfac29
DN
106 console.log(new Date().toISOString() + ' A user joined (', Object.keys(srvSockets).length, ' connected users)');
107 si.currentLoad().then(data => console.log(' Current load: ', data.avgload));
108 si.currentLoad().then(data => console.log(' CPU: ', data.currentload, '%'));
66ec1117
DN
109 // End log server load
110
ba335aa3 111 /*join-rom event permit to join one room.
112 message :
113 userId : user identification
114 roomId: room identification
e8da727c 115 position: position of user in map
116 x: user x position on map
117 y: user y position on map
ba335aa3 118 */
ab798b1c 119 socket.on(SockerIoEvent.JOIN_ROOM, (message: any, answerFn): void => {
256fa51e 120 try {
ab798b1c
DN
121 let roomId = message.roomId;
122
cdfa9acf 123 if (typeof(roomId) !== 'string') {
1bbd0866
DN
124 socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: 'Expected roomId as a string.'});
125 return;
256fa51e 126 }
ab798b1c
DN
127 let position = this.hydratePositionReceive(message.position);
128 if (position instanceof Error) {
129 socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: position.message});
130 return;
131 }
b4f77ba5 132
256fa51e 133 let Client = (socket as ExSocketInterface);
8b9c36e3 134
cdfa9acf 135 if (Client.roomId === roomId) {
256fa51e
DN
136 return;
137 }
5f11b065 138
256fa51e
DN
139 //leave previous room
140 this.leaveRoom(Client);
5f11b065 141
256fa51e 142 //join new previous room
ab798b1c 143 let world = this.joinRoom(Client, roomId, position);
b4f77ba5 144
256fa51e 145 //add function to refresh position user in real time.
125a4d11 146 //this.refreshUserPosition(Client);
b4f77ba5 147
0c9cbca7 148 let messageUserJoined = new MessageUserJoined(Client.userId, Client.name, Client.character, Client.position);
cdfa9acf 149
125a4d11
DN
150 socket.to(roomId).emit(SockerIoEvent.JOIN_ROOM, messageUserJoined);
151
152 // The answer shall contain the list of all users of the room with their positions:
153 let listOfUsers = Array.from(world.getUsers(), ([key, user]) => {
154 let player = this.searchClientByIdOrFail(user.id);
155 return new MessageUserPosition(user.id, player.name, player.character, player.position);
156 });
157 answerFn(listOfUsers);
256fa51e
DN
158 } catch (e) {
159 console.error('An error occurred on "join_room" event');
160 console.error(e);
161 }
4e111572 162 });
163
1bbd0866 164 socket.on(SockerIoEvent.USER_POSITION, (message: any): void => {
256fa51e 165 try {
4d1c3517
DN
166 let position = this.hydratePositionReceive(message);
167 if (position instanceof Error) {
1bbd0866
DN
168 socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: position.message});
169 return;
256fa51e
DN
170 }
171
172 let Client = (socket as ExSocketInterface);
173
174 // sending to all clients in room except sender
4d1c3517 175 Client.position = position;
256fa51e 176
125a4d11
DN
177 // update position in the world
178 let world = this.Worlds.get(Client.roomId);
179 if (!world) {
180 console.error("Could not find world with id '", Client.roomId, "'");
181 return;
182 }
ab798b1c 183 world.updatePosition(Client, position);
125a4d11 184
0c9cbca7 185 socket.to(Client.roomId).emit(SockerIoEvent.USER_MOVED, new MessageUserMoved(Client.userId, Client.position));
256fa51e
DN
186 } catch (e) {
187 console.error('An error occurred on "user_position" event');
188 console.error(e);
53e1600e 189 }
787e1c46 190 });
5b62ac39 191
5a3668a1 192 socket.on(SockerIoEvent.WEBRTC_SIGNAL, (data: any) => {
fdb40ec3 193 //send only at user
e934015d
DN
194 let client = this.sockets.get(data.receiverId);
195 if (client === undefined) {
196 console.warn("While exchanging a WebRTC signal: client with id ", data.receiverId, " does not exist. This might be a race condition.");
fdb40ec3 197 return;
a5b5072d 198 }
5a3668a1 199 return client.emit(SockerIoEvent.WEBRTC_SIGNAL, data);
ba335aa3 200 });
5b62ac39 201
5a3668a1 202 socket.on(SockerIoEvent.WEBRTC_OFFER, (data: any) => {
d7d7be9e 203 //send only at user
e934015d
DN
204 let client = this.sockets.get(data.receiverId);
205 if (client === undefined) {
206 console.warn("While exchanging a WebRTC offer: client with id ", data.receiverId, " does not exist. This might be a race condition.");
fdb40ec3 207 return;
d7d7be9e 208 }
5a3668a1 209 client.emit(SockerIoEvent.WEBRTC_OFFER, data);
5b62ac39 210 });
2bfa57b0 211
fdb40ec3 212 socket.on(SockerIoEvent.DISCONNECT, () => {
0c9cbca7 213 let Client = (socket as ExSocketInterface);
256fa51e 214 try {
256fa51e
DN
215 //leave room
216 this.leaveRoom(Client);
217
218 //leave webrtc room
e934015d 219 //socket.leave(Client.webRtcRoomId);
256fa51e
DN
220
221 //delete all socket information
256fa51e
DN
222 delete Client.webRtcRoomId;
223 delete Client.roomId;
224 delete Client.token;
225 delete Client.position;
226 } catch (e) {
227 console.error('An error occurred on "disconnect"');
228 console.error(e);
229 }
0c9cbca7 230 this.sockets.delete(Client.userId);
66ec1117
DN
231
232 // Let's log server load when a user leaves
233 let srvSockets = this.Io.sockets.sockets;
234 console.log('A user left (', Object.keys(srvSockets).length, ' connected users)');
235 si.currentLoad().then(data => console.log('Current load: ', data.avgload));
236 si.currentLoad().then(data => console.log('CPU: ', data.currentload, '%'));
237 // End log server load
2bfa57b0 238 });
4de55243
DN
239
240 // Let's send the user id to the user
b80e3e07
DN
241 socket.on(SockerIoEvent.SET_PLAYER_DETAILS, (playerDetails: SetPlayerDetailsMessage, answerFn) => {
242 let Client = (socket as ExSocketInterface);
243 Client.name = playerDetails.name;
244 Client.character = playerDetails.character;
0c9cbca7 245 answerFn(Client.userId);
b80e3e07 246 });
2bfa57b0 247 });
248 }
249
e934015d
DN
250 searchClientByIdOrFail(userId: string): ExSocketInterface {
251 let client: ExSocketInterface|undefined = this.sockets.get(userId);
252 if (client === undefined) {
253 throw new Error("Could not find user with id " + userId);
6dc309db 254 }
e934015d 255 return client;
372f938b 256 }
257
8b9c36e3 258 leaveRoom(Client : ExSocketInterface){
125a4d11 259 // leave previous room and world
8b9c36e3 260 if(Client.roomId){
0c9cbca7 261 Client.to(Client.roomId).emit(SockerIoEvent.USER_LEFT, Client.userId);
125a4d11 262
8b9c36e3 263 //user leave previous world
264 let world : World|undefined = this.Worlds.get(Client.roomId);
265 if(world){
266 world.leave(Client);
8b9c36e3 267 }
4de55243
DN
268 //user leave previous room
269 Client.leave(Client.roomId);
270 delete Client.roomId;
8b9c36e3 271 }
272 }
cdfa9acf 273
ab798b1c 274 private joinRoom(Client : ExSocketInterface, roomId: string, position: Point): World {
8b9c36e3 275 //join user in room
cdfa9acf
DN
276 Client.join(roomId);
277 Client.roomId = roomId;
ab798b1c 278 Client.position = position;
8b9c36e3 279
280 //check and create new world for a room
125a4d11
DN
281 let world = this.Worlds.get(roomId)
282 if(world === undefined){
283 world = new World((user1: string, group: Group) => {
8b9c36e3 284 this.connectedUser(user1, group);
285 }, (user1: string, group: Group) => {
286 this.disConnectedUser(user1, group);
287 }, MINIMUM_DISTANCE, GROUP_RADIUS, (group: Group) => {
288 this.sendUpdateGroupEvent(group);
289 }, (groupUuid: string, lastUser: UserInterface) => {
290 this.sendDeleteGroupEvent(groupUuid, lastUser);
291 });
cdfa9acf 292 this.Worlds.set(roomId, world);
8b9c36e3 293 }
294
125a4d11
DN
295 // Dispatch groups position to newly connected user
296 world.getGroups().forEach((group: Group) => {
297 Client.emit(SockerIoEvent.GROUP_CREATE_UPDATE, {
298 position: group.getPosition(),
299 groupId: group.getId()
4cca1c1e 300 });
125a4d11
DN
301 });
302 //join world
303 world.join(Client, Client.position);
304 return world;
8b9c36e3 305 }
306
2bfa57b0 307 /**
308 *
309 * @param socket
310 * @param roomId
311 */
372f938b 312 joinWebRtcRoom(socket: ExSocketInterface, roomId: string) {
313 if (socket.webRtcRoomId === roomId) {
2bfa57b0 314 return;
315 }
316 socket.join(roomId);
317 socket.webRtcRoomId = roomId;
59ee7827
DN
318 //if two persons in room share
319 if (this.Io.sockets.adapter.rooms[roomId].length < 2 /*|| this.Io.sockets.adapter.rooms[roomId].length >= 4*/) {
2bfa57b0 320 return;
321 }
802d7100 322 let clients: Array<ExSocketInterface> = (Object.values(this.Io.sockets.sockets) as Array<ExSocketInterface>)
41f5b5a1 323 .filter((client: ExSocketInterface) => client.webRtcRoomId && client.webRtcRoomId === roomId);
2bfa57b0 324 //send start at one client to initialise offer webrtc
325 //send all users in room to create PeerConnection in front
326 clients.forEach((client: ExSocketInterface, index: number) => {
327
328 let clientsId = clients.reduce((tabs: Array<any>, clientId: ExSocketInterface, indexClientId: number) => {
0c9cbca7 329 if (!clientId.userId || clientId.userId === client.userId) {
2bfa57b0 330 return tabs;
331 }
332 tabs.push({
0c9cbca7 333 userId: clientId.userId,
787e1c46 334 name: clientId.name,
2bfa57b0 335 initiator: index <= indexClientId
336 });
337 return tabs;
338 }, []);
339
5a3668a1 340 client.emit(SockerIoEvent.WEBRTC_START, {clients: clientsId, roomId: roomId});
ba335aa3 341 });
342 }
e8da727c 343
53e1600e 344 //Hydrate and manage error
4d1c3517 345 hydratePositionReceive(message: any): Point | Error {
53e1600e 346 try {
b13c9991 347 if (!message.x || !message.y || !message.direction || message.moving === undefined || message.silent === undefined) {
4d1c3517
DN
348 return new Error("invalid point message sent");
349 }
b13c9991 350 return new Point(message.x, message.y, message.direction, message.moving, message.silent);
372f938b 351 } catch (err) {
53e1600e 352 //TODO log error
f04d1342 353 return new Error(err);
53e1600e 354 }
355 }
fbcb48f9 356
357 /** permit to share user position
372f938b 358 ** users position will send in event 'user-position'
359 ** The data sent is an array with information for each user :
360 [
361 {
fbcb48f9 362 userId: <string>,
363 roomId: <string>,
364 position: {
365 x : <number>,
77780bd2 366 y : <number>,
367 direction: <string>
fbcb48f9 368 }
369 },
372f938b 370 ...
371 ]
fbcb48f9 372 **/
881bb04e 373
374 //connected user
372f938b 375 connectedUser(userId: string, group: Group) {
e934015d
DN
376 /*let Client = this.sockets.get(userId);
377 if (Client === undefined) {
e06b20fe 378 return;
e934015d
DN
379 }*/
380 let Client = this.searchClientByIdOrFail(userId);
b13c9991
RR
381 if (Client.position.silent === false){
382 this.joinWebRtcRoom(Client, group.getId());
383 }
881bb04e 384 }
385
02e6b50b 386 //disconnect user
372f938b 387 disConnectedUser(userId: string, group: Group) {
e934015d
DN
388 let Client = this.searchClientByIdOrFail(userId);
389 Client.to(group.getId()).emit(SockerIoEvent.WEBRTC_DISCONNECT, {
390 userId: userId
391 });
392
96c5d92c
DN
393 // Most of the time, sending a disconnect event to one of the players is enough (the player will close the connection
394 // which will be shut for the other player).
395 // However! In the rare case where the WebRTC connection is not yet established, if we close the connection on one of the player,
396 // the other player will try connecting until a timeout happens (during this time, the connection icon will be displayed for nothing).
397 // So we also send the disconnect event to the other player.
398 for (let user of group.getUsers()) {
399 Client.emit(SockerIoEvent.WEBRTC_DISCONNECT, {
400 userId: user.id
401 });
402 }
403
e934015d
DN
404 //disconnect webrtc room
405 if(!Client.webRtcRoomId){
372f938b 406 return;
407 }
e934015d
DN
408 Client.leave(Client.webRtcRoomId);
409 delete Client.webRtcRoomId;
881bb04e 410 }
d064aca5 411}