2020-04-04 04:08:12 +02:00
import * as http from "http" ;
2020-05-15 23:24:04 +02:00
import { MessageUserPosition , Point } from "../Model/Websocket/MessageUserPosition" ; //TODO fix import by "_Model/.."
2020-04-04 17:22:02 +02:00
import { ExSocketInterface } from "../Model/Websocket/ExSocketInterface" ; //TODO fix import by "_Model/.."
2020-04-04 14:05:18 +02:00
import Jwt , { JsonWebTokenError } from "jsonwebtoken" ;
2020-09-11 09:56:05 +02:00
import { SECRET_KEY , MINIMUM_DISTANCE , GROUP_RADIUS , ALLOW_ARTILLERY } from "../Enum/EnvironmentVariable" ; //TODO fix import by "_Enum/..."
2020-04-27 00:44:25 +02:00
import { World } from "../Model/World" ;
2020-09-15 10:06:11 +02:00
import { Group } from "../Model/Group" ;
2020-09-16 16:06:43 +02:00
import { User } from "../Model/User" ;
2020-06-09 23:07:19 +02:00
import { isSetPlayerDetailsMessage , } from "../Model/Websocket/SetPlayerDetailsMessage" ;
2020-05-19 19:11:12 +02:00
import { MessageUserJoined } from "../Model/Websocket/MessageUserJoined" ;
2020-05-28 22:00:56 +02:00
import si from "systeminformation" ;
2020-06-09 11:49:23 +02:00
import { Gauge } from "prom-client" ;
2020-06-09 23:07:19 +02:00
import { TokenInterface } from "../Controller/AuthenticateController" ;
import { isJoinRoomMessageInterface } from "../Model/Websocket/JoinRoomMessage" ;
2020-09-21 11:24:03 +02:00
import { PointInterface } from "../Model/Websocket/PointInterface" ;
2020-06-09 23:07:19 +02:00
import { isWebRtcSignalMessageInterface } from "../Model/Websocket/WebRtcSignalMessage" ;
import { UserInGroupInterface } from "../Model/Websocket/UserInGroupInterface" ;
2020-07-27 22:36:07 +02:00
import { isItemEventMessageInterface } from "../Model/Websocket/ItemEventMessage" ;
2020-09-09 12:32:01 +02:00
import { uuid } from 'uuidv4' ;
2020-09-16 16:06:43 +02:00
import { GroupUpdateInterface } from "_Model/Websocket/GroupUpdateInterface" ;
import { Movable } from "../Model/Movable" ;
2020-09-18 15:51:15 +02:00
import {
PositionMessage ,
SetPlayerDetailsMessage ,
SubMessage ,
UserMovedMessage ,
2020-09-24 17:24:37 +02:00
BatchMessage ,
GroupUpdateMessage ,
PointMessage ,
GroupDeleteMessage ,
UserJoinedMessage ,
UserLeftMessage ,
2020-09-28 18:52:54 +02:00
ItemEventMessage ,
ViewportMessage ,
ClientToServerMessage ,
JoinRoomMessage ,
ErrorMessage ,
RoomJoinedMessage ,
ItemStateMessage ,
2020-09-29 16:01:22 +02:00
ServerToClientMessage ,
SetUserIdMessage ,
SilentMessage ,
WebRtcSignalToClientMessage ,
WebRtcSignalToServerMessage ,
WebRtcStartMessage , WebRtcDisconnectMessage
2020-09-24 11:16:08 +02:00
} from "../Messages/generated/messages_pb" ;
import { UserMovesMessage } from "../Messages/generated/messages_pb" ;
2020-09-18 13:57:38 +02:00
import Direction = PositionMessage . Direction ;
2020-09-18 15:51:15 +02:00
import { ProtobufUtils } from "../Model/Websocket/ProtobufUtils" ;
2020-09-30 10:12:40 +02:00
import { App , HttpRequest , TemplatedApp , WebSocket } from "uWebSockets.js"
import { parse } from "query-string" ;
2020-04-29 01:40:32 +02:00
2020-09-18 13:57:38 +02:00
enum SocketIoEvent {
2020-04-29 01:40:32 +02:00
CONNECTION = "connection" ,
2020-05-02 20:46:02 +02:00
DISCONNECT = "disconnect" ,
2020-05-19 19:11:12 +02:00
JOIN_ROOM = "join-room" , // bi-directional
2020-09-15 10:06:11 +02:00
USER_POSITION = "user-position" , // From client to server
2020-05-19 19:11:12 +02:00
USER_MOVED = "user-moved" , // From server to client
USER_LEFT = "user-left" , // From server to client
2020-04-29 01:40:32 +02:00
WEBRTC_SIGNAL = "webrtc-signal" ,
2020-06-11 23:18:06 +02:00
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal" ,
2020-04-29 01:40:32 +02:00
WEBRTC_START = "webrtc-start" ,
2020-05-02 20:46:02 +02:00
WEBRTC_DISCONNECT = "webrtc-disconect" ,
2020-04-29 01:40:32 +02:00
MESSAGE_ERROR = "message-error" ,
2020-05-08 00:35:36 +02:00
GROUP_CREATE_UPDATE = "group-create-update" ,
GROUP_DELETE = "group-delete" ,
2020-07-27 22:36:07 +02:00
SET_PLAYER_DETAILS = "set-player-details" ,
ITEM_EVENT = 'item-event' ,
2020-08-31 14:03:40 +02:00
SET_SILENT = "set_silent" , // Set or unset the silent mode for this user.
2020-09-15 10:06:11 +02:00
SET_VIEWPORT = "set-viewport" ,
BATCH = "batch" ,
}
2020-09-28 18:52:54 +02:00
function emitInBatch ( socket : ExSocketInterface , payload : SubMessage ) : void {
2020-09-18 15:51:15 +02:00
socket . batchedMessages . addPayload ( payload ) ;
2020-09-15 10:06:11 +02:00
if ( socket . batchTimeout === null ) {
socket . batchTimeout = setTimeout ( ( ) = > {
2020-09-29 09:45:47 +02:00
if ( socket . disconnecting ) {
return ;
}
2020-09-28 18:52:54 +02:00
const serverToClientMessage = new ServerToClientMessage ( ) ;
serverToClientMessage . setBatchmessage ( socket . batchedMessages ) ;
socket . send ( serverToClientMessage . serializeBinary ( ) . buffer , true ) ;
2020-09-18 15:51:15 +02:00
socket . batchedMessages = new BatchMessage ( ) ;
2020-09-15 10:06:11 +02:00
socket . batchTimeout = null ;
} , 100 ) ;
}
2020-04-29 01:40:32 +02:00
}
2020-04-04 04:08:12 +02:00
2020-05-03 16:28:18 +02:00
export class IoSocketController {
2020-06-09 11:49:23 +02:00
private Worlds : Map < string , World > = new Map < string , World > ( ) ;
2020-09-18 13:57:38 +02:00
private sockets : Map < number , ExSocketInterface > = new Map < number , ExSocketInterface > ( ) ;
2020-06-09 11:49:23 +02:00
private nbClientsGauge : Gauge < string > ;
private nbClientsPerRoomGauge : Gauge < string > ;
2020-09-18 13:57:38 +02:00
private nextUserId : number = 1 ;
2020-05-03 16:28:18 +02:00
2020-09-28 18:52:54 +02:00
constructor ( private readonly app : TemplatedApp ) {
2020-06-09 11:49:23 +02:00
this . nbClientsGauge = new Gauge ( {
name : 'workadventure_nb_sockets' ,
help : 'Number of connected sockets' ,
2020-06-29 23:01:52 +02:00
labelNames : [ ]
2020-06-09 11:49:23 +02:00
} ) ;
this . nbClientsPerRoomGauge = new Gauge ( {
name : 'workadventure_nb_clients_per_room' ,
help : 'Number of clients per room' ,
2020-06-29 23:01:52 +02:00
labelNames : [ 'room' ]
2020-06-09 11:49:23 +02:00
} ) ;
2020-04-04 14:05:18 +02:00
2020-04-04 04:08:12 +02:00
this . ioConnection ( ) ;
2020-05-08 00:35:36 +02:00
}
2020-06-09 23:07:19 +02:00
private isValidToken ( token : object ) : token is TokenInterface {
2020-09-18 13:57:38 +02:00
if ( typeof ( ( token as TokenInterface ) . userUuid ) !== 'string' ) {
2020-06-09 23:07:19 +02:00
return false ;
}
return true ;
}
2020-05-23 14:00:36 +02:00
/ * *
*
* @param token
* /
2020-09-29 10:57:14 +02:00
/ * s e a r c h C l i e n t B y T o k e n ( t o k e n : s t r i n g ) : E x S o c k e t I n t e r f a c e | n u l l {
2020-06-09 23:07:19 +02:00
const clients : ExSocketInterface [ ] = Object . values ( this . Io . sockets . sockets ) as ExSocketInterface [ ] ;
2020-05-23 14:00:36 +02:00
for ( let i = 0 ; i < clients . length ; i ++ ) {
2020-06-09 23:07:19 +02:00
const client = clients [ i ] ;
2020-05-23 14:00:36 +02:00
if ( client . token !== token ) {
continue
}
return client ;
}
return null ;
2020-09-29 10:57:14 +02:00
} * /
2020-05-23 14:00:36 +02:00
2020-09-30 10:12:40 +02:00
private async authenticate ( req : HttpRequest ) : Promise < { token : string , userUuid : string } > {
2020-09-28 18:52:54 +02:00
//console.log(socket.handshake.query.token);
2020-09-16 11:41:03 +02:00
2020-09-30 10:12:40 +02:00
const query = parse ( req . getQuery ( ) ) ;
if ( ! query . token ) {
console . error ( 'An authentication error happened, a user tried to connect without a token.' ) ;
throw new Error ( 'An authentication error happened, a user tried to connect without a token.' ) ;
}
const token = query . token ;
if ( typeof ( token ) !== "string" ) {
throw new Error ( 'Token is expected to be a string' ) ;
}
if ( token === 'test' ) {
if ( ALLOW_ARTILLERY ) {
return {
token ,
userUuid : uuid ( )
2020-09-11 09:56:05 +02:00
}
2020-09-30 10:12:40 +02:00
} else {
throw new Error ( "In order to perform a load-testing test on this environment, you must set the ALLOW_ARTILLERY environment variable to 'true'" ) ;
2020-09-09 12:32:01 +02:00
}
2020-09-30 10:12:40 +02:00
}
/ * i f ( t h i s . s e a r c h C l i e n t B y T o k e n ( s o c k e t . h a n d s h a k e . q u e r y . t o k e n ) ) {
console . error ( 'An authentication error happened, a user tried to connect while its token is already connected.' ) ;
return next ( new Error ( 'Authentication error' ) ) ;
} * /
const promise = new Promise < { token : string , userUuid : string } > ( ( resolve , reject ) = > {
Jwt . verify ( token , SECRET_KEY , ( err : JsonWebTokenError , tokenDecoded : object ) = > {
2020-09-28 15:02:37 +02:00
const tokenInterface = tokenDecoded as TokenInterface ;
2020-04-04 14:05:18 +02:00
if ( err ) {
2020-06-10 12:32:39 +02:00
console . error ( 'An authentication error happened, invalid JsonWebToken.' , err ) ;
2020-09-30 10:12:40 +02:00
reject ( new Error ( 'An authentication error happened, invalid JsonWebToken. ' + err . message ) ) ;
return ;
2020-04-04 14:05:18 +02:00
}
2020-09-15 16:21:41 +02:00
2020-09-28 15:02:37 +02:00
if ( ! this . isValidToken ( tokenInterface ) ) {
2020-09-30 10:12:40 +02:00
reject ( new Error ( 'Authentication error, invalid token structure.' ) ) ;
return ;
2020-06-09 23:07:19 +02:00
}
2020-09-24 17:36:10 +02:00
2020-09-30 10:12:40 +02:00
resolve ( {
token ,
userUuid : tokenInterface.userUuid
} ) ;
} ) ;
} ) ;
return promise ;
2020-09-28 18:52:54 +02:00
}
2020-09-15 16:21:41 +02:00
2020-09-28 18:52:54 +02:00
ioConnection() {
this . app . ws ( '/*' , {
2020-09-30 10:12:40 +02:00
2020-09-28 18:52:54 +02:00
/* Options */
//compression: uWS.SHARED_COMPRESSOR,
maxPayloadLength : 16 * 1024 * 1024 ,
2020-09-29 16:01:22 +02:00
//idleTimeout: 10,
2020-09-30 10:12:40 +02:00
upgrade : ( res , req , context ) = > {
console . log ( 'An Http connection wants to become WebSocket, URL: ' + req . getUrl ( ) + '!' ) ;
( async ( ) = > {
/* Keep track of abortions */
const upgradeAborted = { aborted : false } ;
res . onAborted ( ( ) = > {
/* We can simply signal that we were aborted */
upgradeAborted . aborted = true ;
} ) ;
try {
const result = await this . authenticate ( req ) ;
if ( upgradeAborted . aborted ) {
console . log ( "Ouch! Client disconnected before we could upgrade it!" ) ;
/* You must not upgrade now */
return ;
}
/* This immediately calls open handler, you must not use res after this call */
res . upgrade ( {
// Data passed here is accessible on the "websocket" socket object.
url : req.getUrl ( ) ,
token : result.token ,
userUuid : result.userUuid
} ,
/* Spell these correctly */
req . getHeader ( 'sec-websocket-key' ) ,
req . getHeader ( 'sec-websocket-protocol' ) ,
req . getHeader ( 'sec-websocket-extensions' ) ,
context ) ;
2020-09-30 10:17:01 +02:00
} catch ( e ) {
2020-09-30 10:12:40 +02:00
if ( e instanceof Error ) {
console . warn ( e . message ) ;
res . writeStatus ( "401 Unauthorized" ) . end ( e . message ) ;
} else {
console . warn ( e ) ;
res . writeStatus ( "500 Internal Server Error" ) . end ( 'An error occurred' ) ;
}
return ;
}
} ) ( ) ;
} ,
2020-09-28 18:52:54 +02:00
/* Handlers */
open : ( ws ) = > {
const client : ExSocketInterface = ws as ExSocketInterface ;
2020-09-30 10:12:40 +02:00
client . userId = this . nextUserId ;
this . nextUserId ++ ;
client . userUuid = ws . userUuid ;
client . token = ws . token ;
2020-09-28 18:52:54 +02:00
client . batchedMessages = new BatchMessage ( ) ;
client . batchTimeout = null ;
client . emitInBatch = ( payload : SubMessage ) : void = > {
emitInBatch ( client , payload ) ;
2020-09-15 16:21:41 +02:00
}
2020-09-28 18:52:54 +02:00
client . disconnecting = false ;
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)' ) ;
} ,
message : ( ws , arrayBuffer , isBinary ) = > {
const client = ws as ExSocketInterface ;
const message = ClientToServerMessage . deserializeBinary ( new Uint8Array ( arrayBuffer ) ) ;
if ( message . hasJoinroommessage ( ) ) {
this . handleJoinRoom ( client , message . getJoinroommessage ( ) as JoinRoomMessage ) ;
} else if ( message . hasViewportmessage ( ) ) {
this . handleViewport ( client , message . getViewportmessage ( ) as ViewportMessage ) ;
} else if ( message . hasUsermovesmessage ( ) ) {
this . handleUserMovesMessage ( client , message . getUsermovesmessage ( ) as UserMovesMessage ) ;
} else if ( message . hasSetplayerdetailsmessage ( ) ) {
this . handleSetPlayerDetails ( client , message . getSetplayerdetailsmessage ( ) as SetPlayerDetailsMessage ) ;
} else if ( message . hasSilentmessage ( ) ) {
this . handleSilentMessage ( client , message . getSilentmessage ( ) as SilentMessage ) ;
} else if ( message . hasItemeventmessage ( ) ) {
this . handleItemEvent ( client , message . getItemeventmessage ( ) as ItemEventMessage ) ;
2020-09-29 16:01:22 +02:00
} else if ( message . hasWebrtcsignaltoservermessage ( ) ) {
this . emitVideo ( client , message . getWebrtcsignaltoservermessage ( ) as WebRtcSignalToServerMessage )
} else if ( message . hasWebrtcscreensharingsignaltoservermessage ( ) ) {
this . emitScreenSharing ( client , message . getWebrtcscreensharingsignaltoservermessage ( ) as WebRtcSignalToServerMessage )
2020-04-04 17:22:02 +02:00
}
2020-04-19 19:32:38 +02:00
2020-09-28 18:52:54 +02:00
/* Ok is false if backpressure was built up, wait for drain */
//let ok = ws.send(message, isBinary);
} ,
drain : ( ws ) = > {
console . log ( 'WebSocket backpressure: ' + ws . getBufferedAmount ( ) ) ;
} ,
close : ( ws , code , message ) = > {
const Client = ( ws as ExSocketInterface ) ;
2020-05-12 11:49:55 +02:00
try {
2020-09-28 18:52:54 +02:00
Client . disconnecting = true ;
2020-05-12 11:49:55 +02:00
//leave room
this . leaveRoom ( Client ) ;
//delete all socket information
2020-09-29 17:24:16 +02:00
/ * d e l e t e C l i e n t . r o o m I d ;
2020-05-12 11:49:55 +02:00
delete Client . token ;
2020-09-29 17:24:16 +02:00
delete Client . position ; * /
2020-05-12 11:49:55 +02:00
} catch ( e ) {
console . error ( 'An error occurred on "disconnect"' ) ;
console . error ( e ) ;
}
2020-09-28 18:52:54 +02:00
2020-05-23 15:04:25 +02:00
this . sockets . delete ( Client . userId ) ;
2020-05-28 22:00:56 +02:00
// Let's log server load when a user leaves
2020-06-29 23:01:52 +02:00
this . nbClientsGauge . dec ( ) ;
2020-09-28 18:52:54 +02:00
console . log ( 'A user left (' , this . sockets . size , ' connected users)' ) ;
}
} )
// TODO: finish this!
/ * t h i s . I o . o n ( S o c k e t I o E v e n t . C O N N E C T I O N , ( s o c k e t : S o c k e t ) = > {
socket . on ( SocketIoEvent . WEBRTC_SIGNAL , ( data : unknown ) = > {
this . emitVideo ( ( socket as ExSocketInterface ) , data ) ;
2020-04-29 01:40:32 +02:00
} ) ;
2020-05-14 23:19:48 +02:00
2020-09-28 18:52:54 +02:00
socket . on ( SocketIoEvent . WEBRTC_SCREEN_SHARING_SIGNAL , ( data : unknown ) = > {
this . emitScreenSharing ( ( socket as ExSocketInterface ) , data ) ;
2020-05-15 22:40:06 +02:00
} ) ;
2020-08-31 14:03:40 +02:00
2020-09-28 18:52:54 +02:00
} ) ; * /
}
2020-08-31 14:03:40 +02:00
2020-09-28 18:52:54 +02:00
private emitError ( Client : ExSocketInterface , message : string ) : void {
const errorMessage = new ErrorMessage ( ) ;
errorMessage . setMessage ( message ) ;
2020-08-31 14:03:40 +02:00
2020-09-28 18:52:54 +02:00
const serverToClientMessage = new ServerToClientMessage ( ) ;
serverToClientMessage . setErrormessage ( errorMessage ) ;
2020-07-27 22:36:07 +02:00
2020-09-28 18:52:54 +02:00
if ( ! Client . disconnecting ) {
2020-09-29 16:01:22 +02:00
Client . send ( serverToClientMessage . serializeBinary ( ) . buffer , true ) ;
2020-09-28 18:52:54 +02:00
}
console . warn ( message ) ;
}
2020-09-24 17:24:37 +02:00
2020-09-28 18:52:54 +02:00
private handleJoinRoom ( Client : ExSocketInterface , message : JoinRoomMessage ) : void {
try {
/ * i f ( ! i s J o i n R o o m M e s s a g e I n t e r f a c e ( m e s s a g e . t o O b j e c t ( ) ) ) {
console . log ( message . toObject ( ) )
this . emitError ( Client , 'Invalid JOIN_ROOM message received: ' + message . toObject ( ) . toString ( ) ) ;
return ;
} * /
const roomId = message . getRoomid ( ) ;
if ( Client . roomId === roomId ) {
return ;
}
2020-09-24 17:24:37 +02:00
2020-09-28 18:52:54 +02:00
//leave previous room
2020-09-29 17:24:16 +02:00
//this.leaveRoom(Client); // Useless now, there is only one room per connection
2020-07-27 22:36:07 +02:00
2020-09-28 18:52:54 +02:00
//join new previous room
const world = this . joinRoom ( Client , roomId , ProtobufUtils . toPointInterface ( message . getPosition ( ) as PositionMessage ) ) ;
2020-07-27 22:36:07 +02:00
2020-09-28 18:52:54 +02:00
const things = world . setViewport ( Client , ( message . getViewport ( ) as ViewportMessage ) . toObject ( ) ) ;
2020-09-24 17:24:37 +02:00
2020-09-28 18:52:54 +02:00
const roomJoinedMessage = new RoomJoinedMessage ( ) ;
2020-09-24 17:24:37 +02:00
2020-09-28 18:52:54 +02:00
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 ;
2020-09-24 17:24:37 +02:00
}
2020-09-28 18:52:54 +02:00
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" ) ;
2020-07-27 22:36:07 +02:00
}
2020-09-28 18:52:54 +02:00
}
for ( const [ itemId , item ] of world . getItemsState ( ) . entries ( ) ) {
const itemStateMessage = new ItemStateMessage ( ) ;
itemStateMessage . setItemid ( itemId ) ;
itemStateMessage . setStatejson ( JSON . stringify ( item ) ) ;
roomJoinedMessage . addItem ( itemStateMessage ) ;
}
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 ( ) ;
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 ) ;
}
}
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 ;
const setUserIdMessage = new SetUserIdMessage ( ) ;
setUserIdMessage . setUserid ( client . userId ) ;
const serverToClientMessage = new ServerToClientMessage ( ) ;
serverToClientMessage . setSetuseridmessage ( setUserIdMessage ) ;
if ( ! client . disconnecting ) {
client . send ( serverToClientMessage . serializeBinary ( ) . buffer , true ) ;
}
}
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 ) ;
}
2020-04-29 01:40:32 +02:00
}
2020-09-29 16:01:22 +02:00
emitVideo ( socket : ExSocketInterface , data : WebRtcSignalToServerMessage ) : void {
2020-06-11 23:18:06 +02:00
//send only at user
2020-09-29 16:01:22 +02:00
const client = this . sockets . get ( data . getReceiverid ( ) ) ;
2020-06-11 23:18:06 +02:00
if ( client === undefined ) {
2020-09-29 16:01:22 +02:00
console . warn ( "While exchanging a WebRTC signal: client with id " , data . getReceiverid ( ) , " does not exist. This might be a race condition." ) ;
2020-06-11 23:18:06 +02:00
return ;
}
2020-09-29 16:01:22 +02:00
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 ) ;
2020-06-14 14:47:16 +02:00
}
2020-09-29 16:01:22 +02:00
}
emitScreenSharing ( socket : ExSocketInterface , data : WebRtcSignalToServerMessage ) : void {
2020-08-20 00:05:00 +02:00
//send only at user
2020-09-29 16:01:22 +02:00
const client = this . sockets . get ( data . getReceiverid ( ) ) ;
2020-08-20 00:05:00 +02:00
if ( client === undefined ) {
2020-09-29 16:01:22 +02:00
console . warn ( "While exchanging a WEBRTC_SCREEN_SHARING signal: client with id " , data . getReceiverid ( ) , " does not exist. This might be a race condition." ) ;
2020-08-20 00:05:00 +02:00
return ;
2020-06-14 20:53:18 +02:00
}
2020-09-29 16:01:22 +02:00
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 ) ;
}
2020-06-14 14:47:16 +02:00
}
2020-09-18 13:57:38 +02:00
searchClientByIdOrFail ( userId : number ) : ExSocketInterface {
2020-06-09 15:54:54 +02:00
const client : ExSocketInterface | undefined = this . sockets . get ( userId ) ;
2020-05-18 18:33:04 +02:00
if ( client === undefined ) {
throw new Error ( "Could not find user with id " + userId ) ;
2020-05-08 21:17:52 +02:00
}
2020-05-18 18:33:04 +02:00
return client ;
2020-05-03 16:28:18 +02:00
}
2020-05-10 19:54:41 +02:00
leaveRoom ( Client : ExSocketInterface ) {
2020-05-19 19:11:12 +02:00
// leave previous room and world
2020-05-10 19:54:41 +02:00
if ( Client . roomId ) {
2020-06-29 22:56:41 +02:00
try {
//user leave previous world
const world : World | undefined = this . Worlds . get ( Client . roomId ) ;
if ( world ) {
world . leave ( Client ) ;
if ( world . isEmpty ( ) ) {
this . Worlds . delete ( Client . roomId ) ;
}
2020-06-29 19:14:54 +02:00
}
2020-06-29 22:56:41 +02:00
//user leave previous room
2020-09-28 18:52:54 +02:00
//Client.leave(Client.roomId);
2020-06-29 22:56:41 +02:00
} finally {
2020-06-29 23:01:52 +02:00
this . nbClientsPerRoomGauge . dec ( { room : Client.roomId } ) ;
2020-09-29 17:24:16 +02:00
//delete Client.roomId;
2020-05-10 19:54:41 +02:00
}
}
}
2020-05-15 23:24:04 +02:00
2020-06-09 23:07:19 +02:00
private joinRoom ( Client : ExSocketInterface , roomId : string , position : PointInterface ) : World {
2020-05-10 19:54:41 +02:00
//join user in room
2020-09-28 18:52:54 +02:00
//Client.join(roomId);
2020-06-29 23:01:52 +02:00
this . nbClientsPerRoomGauge . inc ( { room : roomId } ) ;
2020-05-15 23:24:04 +02:00
Client . roomId = roomId ;
2020-05-22 22:59:43 +02:00
Client . position = position ;
2020-05-10 19:54:41 +02:00
//check and create new world for a room
2020-05-19 19:11:12 +02:00
let world = this . Worlds . get ( roomId )
if ( world === undefined ) {
2020-09-29 16:01:22 +02:00
world = new World ( ( user1 : User , group : Group ) = > {
this . joinWebRtcRoom ( user1 , group ) ;
} , ( user1 : User , group : Group ) = > {
2020-05-10 19:54:41 +02:00
this . disConnectedUser ( user1 , group ) ;
2020-09-16 16:13:47 +02:00
} , MINIMUM_DISTANCE , GROUP_RADIUS , ( thing : Movable , listener : User ) = > {
2020-09-15 16:21:41 +02:00
const clientListener = this . searchClientByIdOrFail ( listener . id ) ;
2020-09-16 16:06:43 +02:00
if ( thing instanceof User ) {
const clientUser = this . searchClientByIdOrFail ( thing . id ) ;
2020-09-24 14:50:28 +02:00
const userJoinedMessage = new UserJoinedMessage ( ) ;
2020-09-28 18:52:54 +02:00
if ( ! Number . isInteger ( clientUser . userId ) ) {
throw new Error ( 'clientUser.userId is not an integer ' + clientUser . userId ) ;
}
2020-09-24 14:50:28 +02:00
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 ) ;
2020-09-28 18:52:54 +02:00
emitInBatch ( clientListener , subMessage ) ;
2020-09-16 16:06:43 +02:00
} else if ( thing instanceof Group ) {
2020-09-21 11:24:03 +02:00
this . emitCreateUpdateGroupEvent ( clientListener , thing ) ;
2020-09-16 16:06:43 +02:00
} else {
console . error ( 'Unexpected type for Movable.' ) ;
}
} , ( thing : Movable , position , listener ) = > {
2020-09-15 16:21:41 +02:00
const clientListener = this . searchClientByIdOrFail ( listener . id ) ;
2020-09-16 16:06:43 +02:00
if ( thing instanceof User ) {
const clientUser = this . searchClientByIdOrFail ( thing . id ) ;
2020-09-18 15:51:15 +02:00
const userMovedMessage = new UserMovedMessage ( ) ;
userMovedMessage . setUserid ( clientUser . userId ) ;
2020-09-18 18:16:26 +02:00
userMovedMessage . setPosition ( ProtobufUtils . toPositionMessage ( clientUser . position ) ) ;
2020-09-18 15:51:15 +02:00
const subMessage = new SubMessage ( ) ;
subMessage . setUsermovedmessage ( userMovedMessage ) ;
2020-09-28 18:52:54 +02:00
clientListener . emitInBatch ( subMessage ) ;
2020-09-16 16:06:43 +02:00
//console.log("Sending USER_MOVED event");
} else if ( thing instanceof Group ) {
2020-09-21 11:24:03 +02:00
this . emitCreateUpdateGroupEvent ( clientListener , thing ) ;
2020-09-16 16:06:43 +02:00
} else {
console . error ( 'Unexpected type for Movable.' ) ;
}
} , ( thing : Movable , listener ) = > {
2020-09-15 16:21:41 +02:00
const clientListener = this . searchClientByIdOrFail ( listener . id ) ;
2020-09-16 16:06:43 +02:00
if ( thing instanceof User ) {
const clientUser = this . searchClientByIdOrFail ( thing . id ) ;
2020-09-24 16:11:47 +02:00
this . emitUserLeftEvent ( clientListener , clientUser . userId ) ;
2020-09-16 16:06:43 +02:00
} else if ( thing instanceof Group ) {
2020-09-24 10:05:16 +02:00
this . emitDeleteGroupEvent ( clientListener , thing . getId ( ) ) ;
2020-09-16 16:06:43 +02:00
} else {
console . error ( 'Unexpected type for Movable.' ) ;
}
2020-09-15 16:21:41 +02:00
2020-05-10 19:54:41 +02:00
} ) ;
2020-05-15 23:24:04 +02:00
this . Worlds . set ( roomId , world ) ;
2020-05-10 19:54:41 +02:00
}
2020-05-19 19:11:12 +02:00
// Dispatch groups position to newly connected user
world . getGroups ( ) . forEach ( ( group : Group ) = > {
2020-09-24 10:05:16 +02:00
this . emitCreateUpdateGroupEvent ( Client , group ) ;
2020-05-19 19:11:12 +02:00
} ) ;
//join world
world . join ( Client , Client . position ) ;
return world ;
2020-05-10 19:54:41 +02:00
}
2020-09-28 18:52:54 +02:00
private emitCreateUpdateGroupEvent ( client : ExSocketInterface , group : Group ) : void {
2020-09-21 11:24:03 +02:00
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 ) ;
2020-09-24 10:05:16 +02:00
const subMessage = new SubMessage ( ) ;
subMessage . setGroupupdatemessage ( groupUpdateMessage ) ;
2020-09-28 18:52:54 +02:00
emitInBatch ( client , subMessage ) ;
2020-09-24 10:05:16 +02:00
//socket.emit(SocketIoEvent.GROUP_CREATE_UPDATE, groupUpdateMessage.serializeBinary().buffer);
}
2020-09-29 10:57:14 +02:00
private emitDeleteGroupEvent ( client : ExSocketInterface , groupId : number ) : void {
2020-09-24 10:05:16 +02:00
const groupDeleteMessage = new GroupDeleteMessage ( ) ;
groupDeleteMessage . setGroupid ( groupId ) ;
const subMessage = new SubMessage ( ) ;
subMessage . setGroupdeletemessage ( groupDeleteMessage ) ;
2020-09-28 18:52:54 +02:00
emitInBatch ( client , subMessage ) ;
2020-09-21 11:24:03 +02:00
}
2020-09-29 10:57:14 +02:00
private emitUserLeftEvent ( client : ExSocketInterface , userId : number ) : void {
2020-09-24 16:11:47 +02:00
const userLeftMessage = new UserLeftMessage ( ) ;
userLeftMessage . setUserid ( userId ) ;
const subMessage = new SubMessage ( ) ;
subMessage . setUserleftmessage ( userLeftMessage ) ;
2020-09-28 18:52:54 +02:00
emitInBatch ( client , subMessage ) ;
2020-09-24 16:11:47 +02:00
}
2020-09-29 16:01:22 +02:00
joinWebRtcRoom ( user : User , group : Group ) {
/ * c o n s t r o o m I d : s t r i n g = " w e b r t c r o o m " + g r o u p . g e t I d ( ) ;
if ( user . socket . webRtcRoomId === roomId ) {
return ;
} * /
2020-09-28 18:52:54 +02:00
2020-09-29 16:01:22 +02:00
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 ) ;
2020-09-29 17:24:16 +02:00
//console.log('Sending webrtcstart initiator to '+user.socket.userId)
2020-09-29 16:01:22 +02:00
}
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 ) ;
2020-09-29 17:24:16 +02:00
//console.log('Sending webrtcstart to '+otherUser.socket.userId)
2020-09-29 16:01:22 +02:00
}
2020-09-28 18:52:54 +02:00
2020-04-29 01:40:32 +02:00
}
2020-09-29 16:01:22 +02:00
/ * s o c k e t . j o i n ( r o o m I d ) ;
2020-04-29 01:40:32 +02:00
socket . webRtcRoomId = roomId ;
2020-05-13 09:39:48 +02:00
//if two persons in room share
2020-09-29 09:45:47 +02:00
if ( this . Io . sockets . adapter . rooms [ roomId ] . length < 2 ) {
2020-04-29 01:40:32 +02:00
return ;
}
2020-08-20 00:05:00 +02:00
// TODO: scanning all sockets is maybe not the most efficient
2020-06-09 15:54:54 +02:00
const clients : Array < ExSocketInterface > = ( Object . values ( this . Io . sockets . sockets ) as Array < ExSocketInterface > )
2020-05-08 11:54:47 +02:00
. filter ( ( client : ExSocketInterface ) = > client . webRtcRoomId && client . webRtcRoomId === roomId ) ;
2020-04-29 01:40:32 +02:00
//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 ) = > {
2020-08-20 00:05:00 +02:00
const peerClients = clients . reduce ( ( tabs : Array < UserInGroupInterface > , clientId : ExSocketInterface , indexClientId : number ) = > {
2020-05-23 15:04:25 +02:00
if ( ! clientId . userId || clientId . userId === client . userId ) {
2020-04-29 01:40:32 +02:00
return tabs ;
}
tabs . push ( {
2020-05-23 15:04:25 +02:00
userId : clientId.userId ,
2020-05-14 20:39:30 +02:00
name : clientId.name ,
2020-04-29 01:40:32 +02:00
initiator : index <= indexClientId
} ) ;
return tabs ;
} , [ ] ) ;
2020-09-18 13:57:38 +02:00
client . emit ( SocketIoEvent . WEBRTC_START , { clients : peerClients , roomId : roomId } ) ;
2020-09-29 09:45:47 +02:00
} ) ; * /
2020-04-04 04:08:12 +02:00
}
2020-04-04 16:25:03 +02:00
2020-04-04 19:25:08 +02:00
/ * * p e r m i t t o s h a r e u s e r p o s i t i o n
2020-05-03 16:28:18 +02:00
* * users position will send in event 'user-position'
* * The data sent is an array with information for each user :
[
{
2020-04-04 19:25:08 +02:00
userId : < string > ,
roomId : < string > ,
position : {
x : < number > ,
2020-04-07 21:03:33 +02:00
y : < number > ,
direction : < string >
2020-04-04 19:25:08 +02:00
}
} ,
2020-05-03 16:28:18 +02:00
. . .
]
2020-04-04 19:25:08 +02:00
* * /
2020-04-27 00:44:25 +02:00
2020-05-08 00:35:36 +02:00
//disconnect user
2020-09-29 16:01:22 +02:00
disConnectedUser ( user : User , group : Group ) {
2020-09-28 18:52:54 +02:00
2020-09-29 16:01:22 +02:00
const Client = user . socket ;
2020-05-18 18:33:04 +02:00
2020-06-05 13:07:18 +02:00
// 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.
2020-09-29 16:01:22 +02:00
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 ) ;
}
2020-06-05 13:07:18 +02:00
}
2020-05-18 18:33:04 +02:00
//disconnect webrtc room
2020-09-29 16:01:22 +02:00
/ * i f ( ! C l i e n t . w e b R t c R o o m I d ) {
2020-05-03 16:28:18 +02:00
return ;
2020-09-29 16:01:22 +02:00
} * /
//Client.leave(Client.webRtcRoomId);
//delete Client.webRtcRoomId;
2020-04-27 00:44:25 +02:00
}
2020-09-25 13:48:02 +02:00
public getWorlds ( ) : Map < string , World > {
return this . Worlds ;
}
2020-09-28 18:52:54 +02:00
2020-04-04 22:35:20 +02:00
}