2022-10-18 23:24:13 +00:00
'use strict' ;
const config = require ( 'config' ) ;
const { setTimeout : delay } = require ( 'timers/promises' ) ;
const bhttp = require ( 'bhttp' ) ;
2024-01-13 00:29:10 +00:00
const WebSocket = require ( 'ws' ) ;
2022-10-18 23:24:13 +00:00
const logger = require ( 'simple-node-logger' ) . createSimpleLogger ( ) ;
const { argv } = require ( 'yargs' ) ;
const {
onMessage ,
getGames ,
} = require ( './play' ) ;
const instance = process . env . NODE _APP _INSTANCE || 'main' ;
logger . setLevel ( argv . level || 'info' ) ;
async function auth ( ) {
const username = config . uniqueUsername ? ` ${ config . user . username } - ${ new Date ( ) . getTime ( ) . toString ( ) . slice ( - 5 ) } ` : config . user . username ;
2024-02-11 16:33:19 +00:00
const res = await bhttp . post ( ` ${ config . api } /session ` , {
2022-10-18 23:24:13 +00:00
... config . user ,
username ,
} , {
encodeJSON : true ,
} ) ;
if ( res . statusCode !== 200 ) {
throw new Error ( ` Failed to authenticate: ${ res . body . toString ( ) } ` ) ;
}
logger . info ( ` Authenticated as ' ${ username } ' with ${ config . api } ` ) ;
return {
user : res . body ,
2024-02-11 16:33:19 +00:00
// auth may return an explicit auth cookie domain, but we connect through the VPN domain that would break the cookie, so don't use a bhttp session and strip the domain
sessionCookie : res . headers [ 'set-cookie' ] [ 0 ] . replace ( /Domain=.*;/ , '' ) ,
2022-10-18 23:24:13 +00:00
} ;
}
2024-02-11 16:33:19 +00:00
async function getWsId ( sessionCookie ) {
const res = await bhttp . get ( ` ${ config . api } /socket ` , {
headers : {
cookie : sessionCookie ,
} ,
} ) ;
2022-10-18 23:24:13 +00:00
if ( res . statusCode !== 200 ) {
throw new Error ( ` Failed to retrieve WebSocket ID: ${ res . body . toString ( ) } ` ) ;
}
return res . body ;
}
function onConnect ( data , bot ) {
bot . socket . transmit ( 'joinRooms' , { rooms : config . channels } ) ;
}
2022-11-26 16:53:27 +00:00
async function onRooms ( { rooms } , bot ) {
2022-10-18 23:24:13 +00:00
logger . info ( ` Joined ${ rooms . map ( ( room ) => room . name ) . join ( ', ' ) } ` ) ;
2022-11-26 16:53:27 +00:00
const usersRes = await bhttp . get ( ` ${ config . api } /room/ ${ rooms . map ( ( room ) => room . id ) . join ( ',' ) } /users ` ) ;
const users = usersRes . body ;
2023-07-11 14:41:38 +00:00
const userIdsByRoom = Object . values ( users ) . reduce ( ( acc , user ) => {
user . sharedRooms . forEach ( ( roomId ) => {
if ( ! acc [ roomId ] ) {
acc [ roomId ] = [ ] ;
}
acc [ roomId ] . push ( user . id ) ;
} ) ;
return acc ;
} , { } ) ;
2022-10-18 23:24:13 +00:00
/* eslint-disable no-param-reassign */
2023-07-11 14:41:38 +00:00
bot . rooms = rooms . reduce ( ( acc , room ) => ( {
... acc ,
[ room . id ] : {
... room ,
userIds : userIdsByRoom [ room . id ] ,
} ,
} ) , { } ) ;
2022-10-18 23:24:13 +00:00
bot . users = { ... bot . users , ... users } ;
/* eslint-enable no-param-reassign */
rooms . forEach ( ( room ) => {
bot . socket . transmit ( 'message' , {
roomId : room . id ,
body : ` Hi, I am ${ config . user . username } , your game host! ` ,
style : config . style ,
} ) ;
} ) ;
}
/* eslint-disable no-param-reassign */
function onJoin ( data , bot ) {
if ( bot . rooms [ data . roomId ] && ! bot . rooms [ data . roomId ] . userIds ? . includes ( data . user . id ) ) {
bot . users [ data . user . id ] = data . user ;
bot . rooms [ data . roomId ] . userIds . push ( data . user . id ) ;
}
}
function onLeave ( data , bot ) {
if ( bot . rooms [ data . roomId ] ) {
bot . rooms [ data . roomId ] . userIds = bot . rooms [ data . roomId ] . userIds . filter ( ( userId ) => userId !== data . userId ) ;
}
}
const messageHandlers = {
connect : onConnect ,
rooms : onRooms ,
message : onMessage ,
join : onJoin ,
leave : onLeave ,
} ;
function handleError ( error , socket , domain , data ) {
logger . error ( ` ${ domain } ' ${ JSON . stringify ( data ) } ' triggered error: ${ error . message } ${ error . stack } ` ) ;
if ( data ? . roomId ) {
socket . transmit ( 'message' , {
body : ':zap::robot::zap: Many fragments! Some large, some small.' ,
type : 'message' ,
roomId : data . roomId ,
} ) ;
}
}
async function connect ( bot , games ) {
2023-11-13 21:56:46 +00:00
const socket = {
ws : { readyState : 0 } ,
io : { } ,
} ;
2022-10-18 23:24:13 +00:00
socket . connect = async ( ) => {
try {
2024-02-11 16:33:19 +00:00
const { user , sessionCookie } = await auth ( ) ;
const wsCreds = await getWsId ( sessionCookie ) ;
2022-10-18 23:24:13 +00:00
bot . user = user ;
logger . info ( ` Attempting to connect to ${ config . socket } ` ) ;
2024-01-13 00:29:10 +00:00
socket . ws = new WebSocket ( ` ${ config . socket } ? ${ new URLSearchParams ( { v : wsCreds . wsId , t : wsCreds . timestamp } ).toString()} ` , [ ] , {
headers : {
2022-10-18 23:24:13 +00:00
cookie : sessionCookie ,
} ,
} ) ;
2024-01-13 01:26:03 +00:00
socket . ws . on ( 'message' , async ( msgData ) => {
const msg = msgData . toString ( ) ;
if ( typeof msg === 'string' && msg . includes ( 'pong' ) ) {
logger . debug ( ` Received pong ${ msg . split ( ':' ) [ 1 ] } ` ) ;
return ;
}
2022-10-18 23:24:13 +00:00
const [ domain , data ] = JSON . parse ( msg ) ;
logger . debug ( ` Received ${ domain } : ${ JSON . stringify ( data ) } ` ) ;
if ( messageHandlers [ domain ] ) {
try {
await messageHandlers [ domain ] ( data , bot , games ) ;
} catch ( error ) {
handleError ( error , socket , domain , data ) ;
}
}
} ) ;
2024-01-13 00:29:10 +00:00
socket . ws . on ( 'close' , async ( info ) => {
2022-10-18 23:24:13 +00:00
logger . error ( ` WebSocket closed, reconnecting in ${ config . reconnectDelay } seconds: ${ info } ` ) ;
await delay ( config . reconnectDelay * 1000 ) ;
socket . connect ( ) ;
} ) ;
2024-01-13 00:29:10 +00:00
socket . ws . on ( 'error' , async ( error ) => {
logger . error ( ` WebSocket error: ${ error . message } ` ) ;
} ) ;
logger . info ( ` Connected to ${ config . socket } ` ) ;
2022-10-18 23:24:13 +00:00
} catch ( error ) {
logger . error ( ` Failed to connect, retrying in ${ config . reconnectDelay } seconds: ${ error . message } ` ) ;
await delay ( config . reconnectDelay * 1000 ) ;
socket . connect ( ) ;
}
} ;
socket . transmit = ( domain , data ) => {
2024-01-13 00:29:10 +00:00
socket . ws . send ( JSON . stringify ( [ domain , data ] ) ) ;
2022-10-18 23:24:13 +00:00
} ;
2024-01-13 01:26:03 +00:00
function ping ( ) {
setTimeout ( ( ) => {
if ( socket . ws && socket . ws ? . readyState === socket . ws ? . OPEN ) {
const now = Date . now ( ) ;
socket . ws . send ( ` ping: ${ now } ` ) ;
logger . debug ( ` Sent ping ${ now } ` ) ;
}
ping ( ) ;
} , 10000 ) ;
}
ping ( ) ;
2022-10-18 23:24:13 +00:00
socket . connect ( ) ;
return socket ;
}
async function init ( ) {
const bot = {
rooms : [ ] ,
users : [ ] ,
} ;
const games = await getGames ( bot , instance ) ;
bot . socket = await connect ( bot , games ) ;
}
module . exports = init ;