2024-02-26 09:33:55 -05:00
const config = { port : 9091 } ;
var uidcounter = 0 ;
Object . freeze ( config ) ; // Config should not be modified after initialization!
const ChannelStorage = new Map ( ) ;
2024-02-27 10:23:34 -05:00
const UserAttachedChannelStorage = new Map ( ) ; //uid=>channel resolve
const UserAttachedNameStorage = new Map ( ) ; // uid=>username resolve
const ReverseNameToUidStorage = new Map ( ) ; // username=>uid resolve
2024-02-26 09:33:55 -05:00
/** @section guacamole **/
const ParseCommandString = function ( instruction ) {
if ( typeof instruction !== "string" ) {
return ;
}
let position = - 1 ;
let sections = new Array ( ) ;
for ( ; ; ) {
let length = instruction . indexOf ( '.' , position + 1 ) ;
if ( length === - 1 ) {
break ;
}
position = ( parseInt ( instruction . slice ( position + 1 , length ) ) + length ) + 1 ; // todo: not overflow
sections . push ( instruction . slice ( length + 1 , position )
. replace ( /'/g , "'" )
. replace ( /"/g , '"' )
. replace ( ///g , '/' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /&/g , '&' )
) ;
if ( instruction . slice ( position , position + 1 ) === ';' ) {
break ;
}
}
return sections ;
}
const EncodeCommandString = function ( sections ) {
if ( Array . isArray ( sections ) === false ) {
return ;
}
let instruction = new String ( ) ;
const argv = sections ;
for ( let argc = 0 ; argc < sections . length ; argc ++ ) {
if ( typeof sections [ argc ] !== "string" ) {
argv [ argc ] = sections [ argc ] . toString ( ) ;
}
argv [ argc ] = argv [ argc ]
. replace ( /'/g , "'" )
. replace ( /"/g , '"' )
. replace ( /\//g , '/' )
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /&/g , '&' )
;
instruction = instruction . concat ( argv [ argc ] . length . toString ( ) , "." , argv [ argc ] )
instruction += ( argc === sections . length - 1 ) ? ';' : ',' ;
}
return instruction ;
}
/** @section sv_functions **/
const MessageParser = function ( webSocket , message , isBinary ) {
const msg = new TextDecoder ( "utf-8" ) . decode ( message ) ;
if ( isBinary ) {
// TODO: command functions
try {
const args = ParseCommandString ( msg ) ;
switch ( args [ 0 ] ) {
case "rename" : {
// todo: make users be able to rename based on parameters given to rename
var nickname = ` guest ${ Math . floor ( Math . random ( ) * 99999 ) } ` ;
2024-02-27 10:23:34 -05:00
if ( nickname . length > 32 ) { // if it's too long
2024-02-26 09:33:55 -05:00
nickname = ` guest ${ Math . floor ( Math . random ( ) * 99999 ) } ` ;
}
2024-02-27 10:23:34 -05:00
// this should probably be a while or something (only do this if the name exists)
if ( ReverseNameToUidStorage . has ( nickname ) === true ) {
if ( ReverseNameToUidStorage . get ( nickname ) !== webSocket . getUserData ( ) . uid ) {
nickname = ` guest ${ Math . floor ( Math . random ( ) * 99999 ) } ` ;
}
}
2024-02-26 09:33:55 -05:00
UserAttachedNameStorage . set ( webSocket . getUserData ( ) . uid , nickname ) ;
ReverseNameToUidStorage . set ( nickname , webSocket . getUserData ( ) . uid ) ;
webSocket . send ( EncodeCommandString ( [ "rename" , 0 , "" , nickname ] ) , true ) ;
break ;
}
case "connect" : {
if ( args . length <= 1 ) {
webSocket . send ( EncodeCommandString ( [ "connect" , 0 ] ) , true ) ;
break ;
}
let getChannelId = args [ 1 ] ;
if ( server . numSubscribers ( ` channels/ ${ getChannelId } ` ) >= 2 ) {
// this room is full
webSocket . send ( EncodeCommandString ( [ "connect" , 0 ] ) , true ) ;
break ;
}
2024-02-27 10:23:34 -05:00
if ( UserAttachedNameStorage . has ( webSocket . getUserData ( ) . uid ) === false ) {
2024-02-26 09:33:55 -05:00
// user doesn't have a username
webSocket . send ( EncodeCommandString ( [ "connect" , 0 ] ) , true ) ;
break ;
}
2024-02-27 10:23:34 -05:00
if ( ChannelStorage . has ( getChannelId ) === false ) {
2024-02-26 09:33:55 -05:00
// make the channel right then and there
2024-02-27 09:14:08 -05:00
console . log ( ` creating # ${ getChannelId } ` ) ;
2024-02-26 09:33:55 -05:00
ChannelStorage . set ( getChannelId , { "owner" : webSocket . getUserData ( ) . uid } ) ;
} ;
if ( webSocket . isSubscribed ( ` channels/ ${ getChannelId } ` ) === true ) {
// already in this channel
webSocket . send ( EncodeCommandString ( [ "connect" , 0 ] ) , true ) ;
break ;
} ;
2024-02-27 10:23:34 -05:00
if ( UserAttachedChannelStorage . has ( webSocket . getUserData ( ) . uid ) === true ) {
2024-02-26 09:33:55 -05:00
// already in a channel that isn't the one we're trying to join
webSocket . send ( EncodeCommandString ( [ "connect" , 0 ] ) , true ) ;
break ;
}
2024-02-27 09:14:08 -05:00
console . log ( ` user ${ UserAttachedNameStorage . get ( webSocket . getUserData ( ) . uid ) } ( ${ webSocket . getUserData ( ) . uid } ) joined # ${ getChannelId } ` ) ;
2024-02-26 09:33:55 -05:00
webSocket . send ( EncodeCommandString ( [ "connect" , 1 , server . numSubscribers ( ` channels/ ${ getChannelId } ` ) ] ) , true ) ;
webSocket . subscribe ( ` channels/ ${ getChannelId } ` ) ;
UserAttachedChannelStorage . set ( webSocket . getUserData ( ) . uid , getChannelId ) ;
// do we keep adduser?
if ( ChannelStorage . get ( getChannelId ) . owner !== webSocket . getUserData ( ) . uid ) {
2024-02-27 09:14:08 -05:00
// send other user's presense if we don't own the channel (if we're not owner)
2024-02-26 09:33:55 -05:00
webSocket . send ( EncodeCommandString ( [ "adduser" , 1 , UserAttachedNameStorage . get ( ChannelStorage . get ( getChannelId ) . owner ) ] ) , true ) ;
}
// send to other user (or nobody) that we joined this channel
webSocket . publish ( ` channels/ ${ getChannelId } ` , EncodeCommandString ( [ "adduser" , 1 , UserAttachedNameStorage . get ( webSocket . getUserData ( ) . uid ) ] ) , true ) ;
break ;
}
2024-02-27 09:14:08 -05:00
case "disconnect" : {
2024-02-27 10:23:34 -05:00
if ( UserAttachedChannelStorage . has ( webSocket . getUserData ( ) . uid ) === true ) {
2024-02-27 09:14:08 -05:00
console . log ( ` user ${ UserAttachedNameStorage . get ( webSocket . getUserData ( ) . uid ) } ( ${ webSocket . getUserData ( ) . uid } ) left # ${ UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) } ` ) ;
webSocket . unsubscribe ( ` channels/ ${ UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) } ` ) ;
webSocket . publish (
` channels/ ${ UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) } ` ,
EncodeCommandString ( [ "remuser" , UserAttachedNameStorage . get ( webSocket . getUserData ( ) . uid ) ] ) ,
true
) ;
// if room is empty, delete it
if ( server . numSubscribers ( ` channels/ ${ UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) } ` ) === 0 ) {
console . log ( ` deleting # ${ UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) } ` ) ;
ChannelStorage . delete ( UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) ) ;
}
UserAttachedChannelStorage . delete ( webSocket . getUserData ( ) . uid ) ;
}
webSocket . send ( EncodeCommandString ( [ "connect" , 2 ] ) , true ) ; // graceful disconnect message
webSocket . send ( EncodeCommandString ( [ "disconnect" ] ) , true ) ;
return ;
}
2024-02-26 09:33:55 -05:00
case "sync" : {
// client is alive...
break ;
}
default : {
console . warn ( "unknown command" , args ) ;
}
}
} catch ( e ) {
console . log ( e ) ;
webSocket . end ( 1003 , new TextEncoder ( "utf-8" ) . encode ( "Unknown format." ) ) ;
return ;
}
return ;
} else {
// send data to channel
webSocket . publish ( ` channels/ ${ UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) } ` , message , false ) ;
// ask other user if alive still
webSocket . publish ( ` channels/ ${ UserAttachedChannelStorage . get ( webSocket . getUserData ( ) . uid ) } ` , EncodeCommandString ( [ "sync" , ( new Date ) . getTime ( ) ] ) , true ) ;
}
//console.log(`${new TextDecoder("utf-8").decode(webSocket.getRemoteAddressAsText())} message: ${msg}`);
}
const UserStateRequest = function ( webSocket , state , data = null ) {
switch ( state ) {
case "open" : {
//console.log("%s client hello", new TextDecoder("utf-8").decode(webSocket.getRemoteAddressAsText()));
break ;
}
case "close" : {
// close event gets WS disconnect code and the client's termination message (avaliable if data.code is 1000, 3xxx, or 4xxx, but only 1000 is standard)
// but we DO NOT get to access any WS methods, since they've been closed, so we don't actually know who left.
if ( data . code === 1000 ) {
console . log ( ` client exiting, reason: ${ new TextDecoder ( "utf-8" ) . decode ( data . message ) } ` ) ;
}
break ;
}
}
}
/** @section creation **/
const server = require ( "uWebSockets.js" ) . App ( { } ) ;
/** @section config **/
server . ws ( "/connect" , {
// options
"compression" : require ( "uWebSockets.js" ) . DEDICATED _COMPRESSOR _3KB , // do we need a compressor for this kind of protocol, uWS actively despises compression, so it's worth considering.
// here be events
"upgrade" : function ( rs , rq , cx ) {
rs . upgrade ( { "uid" : ( uidcounter ++ ) } , /* very important, this sets a UserData object (which can contain anything we like) to the WSConnection instance. This can only be done here. */
rq . getHeader ( "sec-websocket-key" ) ,
rq . getHeader ( "sec-websocket-protocol" ) ,
rq . getHeader ( "sec-websocket-extensions" ) ,
cx ) ;
} ,
"message" : MessageParser ,
"open" : function ( ws ) { UserStateRequest ( ws , "open" ) } ,
"close" : function ( ws , code , message ) { UserStateRequest ( ws , "close" , { code , message } ) } ,
"drain" : function ( ws ) { console . log ( ` WS going through back-pressure!: ${ ws . getBufferedAmount ( ) } ` ) }
} ) ;
server . listen ( config . port , function ( token ) { console . log ( token ? ` open on ${ config . port } ` : ` failed to listen to ${ config . port } ` ) } ) ;