Refactoring messages
[libreadventure.git] / back / src / Controller / IoSocketController.ts
1 import socketIO = require('socket.io');
2 import {Socket} from "socket.io";
3 import * as http from "http";
4 import {MessageUserPosition} from "../Model/Websocket/MessageUserPosition"; //TODO fix import by "_Model/.."
5 import {ExSocketInterface} from "../Model/Websocket/ExSocketInterface"; //TODO fix import by "_Model/.."
6 import Jwt, {JsonWebTokenError} from "jsonwebtoken";
7 import {SECRET_KEY, MINIMUM_DISTANCE, GROUP_RADIUS} from "../Enum/EnvironmentVariable"; //TODO fix import by "_Enum/..."
8 import {ExtRooms, RefreshUserPositionFunction} from "../Model/Websocket/ExtRooms";
9 import {ExtRoomsInterface} from "../Model/Websocket/ExtRoomsInterface";
10 import {World} from "../Model/World";
11 import {Group} from "_Model/Group";
12 import {UserInterface} from "_Model/UserInterface";
13
14 enum SockerIoEvent {
15 CONNECTION = "connection",
16 DISCONNECT = "disconnect",
17 ATTRIBUTE_USER_ID = "attribute-user-id", // Sent from server to client just after the connexion is established to give the client its unique id.
18 JOIN_ROOM = "join-room",
19 USER_POSITION = "user-position",
20 WEBRTC_SIGNAL = "webrtc-signal",
21 WEBRTC_OFFER = "webrtc-offer",
22 WEBRTC_START = "webrtc-start",
23 WEBRTC_DISCONNECT = "webrtc-disconect",
24 MESSAGE_ERROR = "message-error",
25 GROUP_CREATE_UPDATE = "group-create-update",
26 GROUP_DELETE = "group-delete",
27 }
28
29 export class IoSocketController {
30 Io: socketIO.Server;
31 Worlds: Map<string, World> = new Map<string, World>();
32
33 constructor(server: http.Server) {
34 this.Io = socketIO(server);
35
36 // Authentication with token. it will be decoded and stored in the socket.
37 // Completely commented for now, as we do not use the "/login" route at all.
38 /*this.Io.use((socket: Socket, next) => {
39 if (!socket.handshake.query || !socket.handshake.query.token) {
40 return next(new Error('Authentication error'));
41 }
42 if(this.searchClientByToken(socket.handshake.query.token)){
43 return next(new Error('Authentication error'));
44 }
45 Jwt.verify(socket.handshake.query.token, SECRET_KEY, (err: JsonWebTokenError, tokenDecoded: object) => {
46 if (err) {
47 return next(new Error('Authentication error'));
48 }
49 (socket as ExSocketInterface).token = tokenDecoded;
50 next();
51 });
52 });*/
53
54 this.ioConnection();
55 this.shareUsersPosition();
56 }
57
58 private sendUpdateGroupEvent(group: Group): void {
59 // Let's get the room of the group. To do this, let's get anyone in the group and find its room.
60 // Note: this is suboptimal
61 let userId = group.getUsers()[0].id;
62 let client: ExSocketInterface|null = this.searchClientById(userId);
63 if (client === null) {
64 return;
65 }
66 let roomId = client.roomId;
67 this.Io.in(roomId).emit(SockerIoEvent.GROUP_CREATE_UPDATE, {
68 position: group.getPosition(),
69 groupId: group.getId()
70 });
71 }
72
73 private sendDeleteGroupEvent(uuid: string, lastUser: UserInterface): void {
74 // Let's get the room of the group. To do this, let's get anyone in the group and find its room.
75 // Note: this is suboptimal
76 let userId = lastUser.id;
77 let client: ExSocketInterface|null = this.searchClientById(userId);
78 if (client === null) {
79 console.warn('Could not find client ', userId, ' in group')
80 return;
81 }
82 let roomId = client.roomId;
83 this.Io.in(roomId).emit(SockerIoEvent.GROUP_DELETE, uuid);
84 }
85
86 ioConnection() {
87 this.Io.on(SockerIoEvent.CONNECTION, (socket: Socket) => {
88 /*join-rom event permit to join one room.
89 message :
90 userId : user identification
91 roomId: room identification
92 position: position of user in map
93 x: user x position on map
94 y: user y position on map
95 */
96 socket.on(SockerIoEvent.JOIN_ROOM, (message: string) => {
97 try {
98 let messageUserPosition = this.hydrateMessageReceive(message);
99 if (messageUserPosition instanceof Error) {
100 return socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: messageUserPosition.message})
101 }
102
103 let Client = (socket as ExSocketInterface);
104
105 if (Client.roomId === messageUserPosition.roomId) {
106 return;
107 }
108
109 //leave previous room
110 this.leaveRoom(Client);
111
112 //join new previous room
113 this.joinRoom(Client, messageUserPosition);
114
115 // sending to all clients in room except sender
116 this.saveUserInformation(Client, messageUserPosition);
117
118 //add function to refresh position user in real time.
119 this.refreshUserPosition(Client);
120
121 socket.to(messageUserPosition.roomId).emit(SockerIoEvent.JOIN_ROOM, messageUserPosition.toString());
122 } catch (e) {
123 console.error('An error occurred on "join_room" event');
124 console.error(e);
125 }
126 });
127
128 socket.on(SockerIoEvent.USER_POSITION, (message: string) => {
129 try {
130 let messageUserPosition = this.hydrateMessageReceive(message);
131 if (messageUserPosition instanceof Error) {
132 return socket.emit(SockerIoEvent.MESSAGE_ERROR, {message: messageUserPosition.message});
133 }
134
135 let Client = (socket as ExSocketInterface);
136
137 // sending to all clients in room except sender
138 this.saveUserInformation(Client, messageUserPosition);
139
140 //refresh position of all user in all rooms in real time
141 this.refreshUserPosition(Client);
142 } catch (e) {
143 console.error('An error occurred on "user_position" event');
144 console.error(e);
145 }
146 });
147
148 socket.on(SockerIoEvent.WEBRTC_SIGNAL, (data: any) => {
149 //send only at user
150 let client = this.searchClientById(data.receiverId);
151 if (!client) {
152 console.error("client doesn't exist for ", data.receiverId);
153 return;
154 }
155 return client.emit(SockerIoEvent.WEBRTC_SIGNAL, data);
156 });
157
158 socket.on(SockerIoEvent.WEBRTC_OFFER, (data: any) => {
159
160 //send only at user
161 let client = this.searchClientById(data.receiverId);
162 if (!client) {
163 console.error("client doesn't exist for ", data.receiverId);
164 return;
165 }
166 client.emit(SockerIoEvent.WEBRTC_OFFER, data);
167 });
168
169 socket.on(SockerIoEvent.DISCONNECT, () => {
170 try {
171 let Client = (socket as ExSocketInterface);
172 this.sendDisconnectedEvent(Client);
173
174 //refresh position of all user in all rooms in real time
175 this.refreshUserPosition(Client);
176
177 //leave room
178 this.leaveRoom(Client);
179
180 //leave webrtc room
181 socket.leave(Client.webRtcRoomId);
182
183 //delete all socket information
184 delete Client.webRtcRoomId;
185 delete Client.roomId;
186 delete Client.token;
187 delete Client.position;
188 } catch (e) {
189 console.error('An error occurred on "disconnect"');
190 console.error(e);
191 }
192 });
193
194 // Let's send the user id to the user
195 socket.emit(SockerIoEvent.ATTRIBUTE_USER_ID, socket.id);
196 });
197 }
198
199 /**
200 * TODO: each call to this method is suboptimal. It means that instead of passing an ID, we should pass a client object.
201 * @param userId
202 */
203 searchClientById(userId: string): ExSocketInterface | null {
204 let clients: Array<any> = Object.values(this.Io.sockets.sockets);
205 for (let i = 0; i < clients.length; i++) {
206 let client: ExSocketInterface = clients[i];
207 if (client.id !== userId) {
208 continue;
209 }
210 return client;
211 }
212 console.log("Could not find user with id ", userId);
213 throw new Error("Could not find user with id " + userId);
214 return null;
215 }
216
217 /**
218 * @param userId
219 */
220 searchClientByToken(userId: string): ExSocketInterface | null {
221 let clients: Array<any> = Object.values(this.Io.sockets.sockets);
222 for (let i = 0; i < clients.length; i++) {
223 let client: ExSocketInterface = clients[i];
224 if (client.id !== userId) {
225 continue
226 }
227 return client;
228 }
229 return null;
230 }
231
232 /**
233 *
234 * @param Client: ExSocketInterface
235 */
236 sendDisconnectedEvent(Client: ExSocketInterface) {
237 Client.broadcast.emit(SockerIoEvent.WEBRTC_DISCONNECT, {
238 userId: Client.id
239 });
240
241 //disconnect webrtc room
242 if(!Client.webRtcRoomId){
243 return;
244 }
245 Client.leave(Client.webRtcRoomId);
246 delete Client.webRtcRoomId;
247 }
248
249 /**
250 *
251 * @param Client
252 */
253 leaveRoom(Client : ExSocketInterface){
254 //lease previous room and world
255 if(Client.roomId){
256 //user leave previous world
257 let world : World|undefined = this.Worlds.get(Client.roomId);
258 if(world){
259 world.leave(Client);
260 //this.Worlds.set(Client.roomId, world);
261 }
262 //user leave previous room
263 Client.leave(Client.roomId);
264 delete Client.roomId;
265 }
266 }
267 /**
268 *
269 * @param Client
270 * @param messageUserPosition
271 */
272 joinRoom(Client : ExSocketInterface, messageUserPosition: MessageUserPosition){
273 //join user in room
274 Client.join(messageUserPosition.roomId);
275
276 //check and create new world for a room
277 if(!this.Worlds.get(messageUserPosition.roomId)){
278 let world = new World((user1: string, group: Group) => {
279 this.connectedUser(user1, group);
280 }, (user1: string, group: Group) => {
281 this.disConnectedUser(user1, group);
282 }, MINIMUM_DISTANCE, GROUP_RADIUS, (group: Group) => {
283 this.sendUpdateGroupEvent(group);
284 }, (groupUuid: string, lastUser: UserInterface) => {
285 this.sendDeleteGroupEvent(groupUuid, lastUser);
286 });
287 this.Worlds.set(messageUserPosition.roomId, world);
288 }
289
290 let world : World|undefined = this.Worlds.get(messageUserPosition.roomId);
291
292
293 if(world) {
294 // Dispatch groups position to newly connected user
295 world.getGroups().forEach((group: Group) => {
296 Client.emit(SockerIoEvent.GROUP_CREATE_UPDATE, {
297 position: group.getPosition(),
298 groupId: group.getId()
299 });
300 });
301 //join world
302 world.join(Client, messageUserPosition);
303 this.Worlds.set(messageUserPosition.roomId, world);
304 }
305
306
307 }
308
309 /**
310 *
311 * @param socket
312 * @param roomId
313 */
314 joinWebRtcRoom(socket: ExSocketInterface, roomId: string) {
315 if (socket.webRtcRoomId === roomId) {
316 return;
317 }
318 socket.join(roomId);
319 socket.webRtcRoomId = roomId;
320 //if two persons in room share
321 if (this.Io.sockets.adapter.rooms[roomId].length < 2 /*|| this.Io.sockets.adapter.rooms[roomId].length >= 4*/) {
322 return;
323 }
324 let clients: Array<ExSocketInterface> = (Object.values(this.Io.sockets.sockets) as Array<ExSocketInterface>)
325 .filter((client: ExSocketInterface) => client.webRtcRoomId && client.webRtcRoomId === roomId);
326 //send start at one client to initialise offer webrtc
327 //send all users in room to create PeerConnection in front
328 clients.forEach((client: ExSocketInterface, index: number) => {
329
330 let clientsId = clients.reduce((tabs: Array<any>, clientId: ExSocketInterface, indexClientId: number) => {
331 if (!clientId.id || clientId.id === client.id) {
332 return tabs;
333 }
334 tabs.push({
335 userId: clientId.id,
336 name: clientId.name,
337 initiator: index <= indexClientId
338 });
339 return tabs;
340 }, []);
341
342 client.emit(SockerIoEvent.WEBRTC_START, {clients: clientsId, roomId: roomId});
343 });
344 }
345
346 //permit to save user position in socket
347 saveUserInformation(socket: ExSocketInterface, message: MessageUserPosition) {
348 socket.position = message.position;
349 socket.roomId = message.roomId;
350 //socket.userId = message.userId;
351 socket.name = message.name;
352 socket.character = message.character;
353 }
354
355 refreshUserPosition(Client : ExSocketInterface) {
356 //refresh position of all user in all rooms in real time
357 let rooms = (this.Io.sockets.adapter.rooms as ExtRoomsInterface);
358 if (!rooms.refreshUserPosition) {
359 rooms.refreshUserPosition = RefreshUserPositionFunction;
360 }
361 rooms.refreshUserPosition(rooms, this.Io);
362
363 // update position in the world
364 let data = {
365 userId: Client.id,
366 roomId: Client.roomId,
367 position: Client.position,
368 name: Client.name,
369 character: Client.character,
370 };
371 let messageUserPosition = new MessageUserPosition(data);
372 let world = this.Worlds.get(messageUserPosition.roomId);
373 if (!world) {
374 return;
375 }
376 world.updatePosition(Client, messageUserPosition);
377 this.Worlds.set(messageUserPosition.roomId, world);
378 }
379
380 //Hydrate and manage error
381 hydrateMessageReceive(message: string): MessageUserPosition | Error {
382 try {
383 return new MessageUserPosition(message);
384 } catch (err) {
385 //TODO log error
386 return new Error(err);
387 }
388 }
389
390 /** permit to share user position
391 ** users position will send in event 'user-position'
392 ** The data sent is an array with information for each user :
393 [
394 {
395 userId: <string>,
396 roomId: <string>,
397 position: {
398 x : <number>,
399 y : <number>,
400 direction: <string>
401 }
402 },
403 ...
404 ]
405 **/
406 seTimeOutInProgress: any = null;
407
408 shareUsersPosition() {
409 if (this.seTimeOutInProgress) {
410 clearTimeout(this.seTimeOutInProgress);
411 }
412 //send for each room, all data of position user
413 let arrayMap = (this.Io.sockets.adapter.rooms as ExtRooms).userPositionMapByRoom;
414 if (!arrayMap) {
415 this.seTimeOutInProgress = setTimeout(() => {
416 this.shareUsersPosition();
417 }, 10);
418 return;
419 }
420 arrayMap.forEach((value: any) => {
421 let roomId = value[0];
422 this.Io.in(roomId).emit(SockerIoEvent.USER_POSITION, value);
423 });
424 this.seTimeOutInProgress = setTimeout(() => {
425 this.shareUsersPosition();
426 }, 10);
427 }
428
429 //connected user
430 connectedUser(userId: string, group: Group) {
431 let Client = this.searchClientById(userId);
432 if (!Client) {
433 return;
434 }
435 this.joinWebRtcRoom(Client, group.getId());
436 }
437
438 //disconnect user
439 disConnectedUser(userId: string, group: Group) {
440 let Client = this.searchClientById(userId);
441 if (!Client) {
442 return;
443 }
444 this.sendDisconnectedEvent(Client)
445 }
446 }