Commit | Line | Data |
---|---|---|
ba335aa3 | 1 | import socketIO = require('socket.io'); |
2 | import {Socket} from "socket.io"; | |
3 | import * as http from "http"; | |
cdfa9acf | 4 | import {MessageUserPosition, Point} from "../Model/Websocket/MessageUserPosition"; //TODO fix import by "_Model/.." |
53e1600e | 5 | import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.." |
ba47d8b1 | 6 | import Jwt, {JsonWebTokenError} from "jsonwebtoken"; |
3b27f8b0 | 7 | import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..." |
881bb04e | 8 | import {World} from "../Model/World"; |
e06b20fe | 9 | import {Group} from "_Model/Group"; |
02e6b50b | 10 | import {UserInterface} from "_Model/UserInterface"; |
b80e3e07 | 11 | import {SetPlayerDetailsMessage} from "_Model/Websocket/SetPlayerDetailsMessage"; |
125a4d11 DN |
12 | import {MessageUserJoined} from "../Model/Websocket/MessageUserJoined"; |
13 | import {MessageUserMoved} from "../Model/Websocket/MessageUserMoved"; | |
66ec1117 | 14 | import si from "systeminformation"; |
2bfa57b0 | 15 | |
16 | enum 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 | 33 | export 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 | } |