import * as config from "./config" ; import * as jitsi from "./jitsi" ; import * as log from "./log" ; import * as net from "./net" ; import { prot } from "./protocol" ; import * as ui from "./ui" ; import * as util from "./util" ; import { dce } from "./util" ; const credits = { creditCost : < { currency : number ; credits : number ; } > null , creditRate : < [ number , number ] > null } ; export const users = < { name : string ; online : boolean ; transmitting : boolean ; fullAccess : any ; } [ ] > [ ] ; const sounds = { url2sid : < Record < string , string > > { } } ; export function createMasterInterface ( ) { const masterUI = ui . ui . panels . master ; const persistent = ui . ui . persistent ; persistent . master . style . display = "" ; persistent . userAdmin . style . display = "" ; persistent . masterSpacer . style . display = "" ; if ( ( config . config . format & prot . flags . dataTypeMask ) === prot . flags . dataType . flac ) masterUI . inviteFLAC . checked = true ; else masterUI . inviteFLACHider . style . display = "none" ; if ( config . config . format & config . features . continuous ) masterUI . inviteContinuous . checked = true ; else masterUI . inviteContinuousHider . style . display = "none" ; masterUI . inviteFLAC . onchange = masterUI . inviteContinuous . onchange = genInvite ; masterUI . inviteCopyB . onclick = copyInvite ; genInvite ( ) ; updateMasterAdmin ( ) ; ui . ui . panels . userAdmin . allB . onclick = function ( ) { userAdmin ( - 1 ) ; } ; masterUI . acceptRemoteVideo . checked = masterUI . saveVideoInBrowser . checked = masterUI . downloadVideoLive . checked = config . useVideoRec ; ui . saveConfigCheckbox ( masterUI . acceptRemoteVideo , "master-video-record-host-" + config . useVideoRec , acceptRemoteVideoChange ) ; ui . saveConfigCheckbox ( masterUI . saveVideoInBrowser , "master-video-save-in-browser-" + config . useVideoRec ) ; ui . saveConfigCheckbox ( masterUI . downloadVideoLive , "master-video-download-live-" + config . useVideoRec ) ; configureMasterInterface ( ) ; util . events . addEventListener ( "net.info." + prot . info . mode , function ( ) { configureMasterInterface ( ) ; } ) ; util . events . addEventListener ( "audio.sound" , function ( ev ) { soundButtonUpdate ( ev . detail . url , ev . detail . status , ev . detail . el ) ; } ) ; util . netEvent ( "master" , "info" , function ( ev ) { const msg = ev . detail ; const p = prot . parts . info ; const key = msg . getUint32 ( p . key , true ) ; let val = 0 ; if ( msg . byteLength >= p . length ) val = msg . getUint32 ( p . value , true ) ; switch ( key ) { case prot . info . creditCost : { const v2 = msg . getUint32 ( p . value + 4 , true ) ; credits . creditCost = { currency : val , credits : v2 } ; break ; } case prot . info . creditRate : { const v2 = msg . getUint32 ( p . value + 4 , true ) ; credits . creditRate = [ val , v2 ] ; updateCreditCost ( ) ; break ; } case prot . info . sounds : { const valS = util . decodeText ( msg . buffer . slice ( p . value ) ) ; addSoundButtons ( JSON . parse ( valS ) ) ; break ; } case prot . info . allowAdmin : { if ( msg . byteLength < p . length + 1 ) break ; const allowed = ! ! msg . getUint8 ( p . length ) ; let props = null ; if ( msg . byteLength > p . length + 1 ) { try { props = JSON . parse ( util . decodeText ( msg . buffer . slice ( p . length + 1 ) ) ) ; } catch ( ex ) { } } allowAdmin ( val , allowed , props ) ; break ; } case prot . info . adminState : { if ( msg . byteLength <= p . length ) break ; let props = null ; try { props = JSON . parse ( util . decodeText ( msg . buffer . slice ( p . length ) ) ) ; } catch ( ex ) { } if ( ! props || typeof props !== "object" ) break ; updateAdmin ( val , props ) ; break ; } } } ) ; util . netEvent ( "master" , "user" , function ( ev ) { const msg = ev . detail ; const p = prot . parts . user ; const index = msg . getUint32 ( p . index , true ) ; const status = msg . getUint32 ( p . status , true ) ; const nick = util . decodeText ( msg . buffer . slice ( p . nick ) ) ; if ( status ) { ui . userListAdd ( index , nick , true ) ; while ( users . length <= index ) users . push ( null ) ; users [ index ] = { name : nick , online : true , transmitting : false , fullAccess : null } ; updateMasterAdmin ( ) ; } else { ui . userListRemove ( index , true ) ; if ( users [ index ] ) { users [ index ] . online = false ; users [ index ] . fullAccess = null ; updateMasterAdmin ( ) ; } } } ) ; util . netEvent ( "master" , "speech" , function ( ev ) { const msg = ev . detail ; const p = prot . parts . speech ; const indexStatus = msg . getUint32 ( p . indexStatus , true ) ; const index = indexStatus >>> 1 ; const status = ! ! ( indexStatus & 1 ) ; ui . userListUpdate ( index , status , true ) ; if ( users [ index ] ) { users [ index ] . transmitting = status ; updateMasterAdmin ( ) ; } } ) ; } function configureMasterInterface ( ) { const masterUI = ui . ui . panels . master ; const pauseResume = masterUI . pauseResumeB ; const startStop = masterUI . startStopB ; masterUI . yesNo . style . display = "none" ; pauseResume . disabled = false ; startStop . disabled = false ; if ( net . mode < prot . mode . rec ) { pauseResume . style . display = "none" ; startStop . innerHTML = '<i class="fas fa-microphone-alt"></i> Start recording' ; startStop . onclick = startRecording ; } else if ( net . mode === prot . mode . rec || net . mode === prot . mode . paused ) { pauseResume . style . display = "" ; if ( net . mode === prot . mode . rec ) { pauseResume . innerHTML = '<i class="fas fa-pause"></i> Pause recording' ; pauseResume . onclick = pauseRecording ; } else { pauseResume . innerHTML = '<i class="far fa-pause-circle"></i> Resume recording' ; pauseResume . onclick = resumeRecording ; } startStop . innerHTML = '<i class="fas fa-stop"></i> Stop recording' ; startStop . onclick = stopRecording ; } else { pauseResume . style . display = "none" ; if ( net . mode === prot . mode . buffering ) startStop . innerText = "Waiting for audio from clients..." ; else startStop . innerHTML = '<i class="fas fa-check"></i> Recording finished' ; startStop . onclick = function ( ) { } ; startStop . disabled = true ; } updateCreditCost ( ) ; ui . resizeUI ( ) ; } function sendMode ( mode ) { const p = prot . parts . mode ; const out = new DataView ( new ArrayBuffer ( p . length ) ) ; out . setUint32 ( 0 , prot . ids . mode , true ) ; out . setUint32 ( p . mode , mode , true ) ; net . masterSock . send ( out . buffer ) ; } function startRecording ( ) { ui . ui . panels . master . startStopB . disabled = true ; sendMode ( prot . mode . rec ) ; } function pauseRecording ( ) { ui . ui . panels . master . pauseResumeB . disabled = true ; sendMode ( prot . mode . paused ) ; } function resumeRecording ( ) { ui . ui . panels . master . pauseResumeB . disabled = true ; sendMode ( prot . mode . rec ) ; } function stopRecording ( ) { const startStop = ui . ui . panels . master . startStopB ; startStop . disabled = true ; startStop . innerText = "Are you sure?" ; ui . ui . panels . master . yesNo . style . display = "" ; ui . ui . panels . master . yesB . onclick = stopRecordingYes ; ui . ui . panels . master . yesB . focus ( ) ; ui . ui . panels . master . noB . onclick = stopRecordingNo ; ui . resizeUI ( ) ; } function stopRecordingYes ( ) { ui . ui . panels . master . yesNo . style . display = "none" ; sendMode ( prot . mode . finished ) ; ui . resizeUI ( ) ; } function stopRecordingNo ( ) { configureMasterInterface ( ) ; } function genInvite ( ) { const f = ( ( ui . ui . panels . master . inviteContinuous . checked ? config . features . continuous : 0 ) + ( ( config . config . format & config . features . rtc ) ? config . features . rtc : 0 ) + ( config . useRecordOnly ? config . features . recordOnly : 0 ) + ( config . useVideoRec ? config . features . videorec : 0 ) + ( config . useRTEnnui . audio ? config . features . rtennuiAudio : 0 ) + ( config . useTranscription ? config . features . transcription : 0 ) + ( ui . ui . panels . master . inviteFLAC . checked ? prot . flags . dataType . flac : 0 ) ) ; let sb = "?" + config . iconfig . id . toString ( 36 ) + "-" + config . iconfig . key . toString ( 36 ) ; if ( config . iconfig . port ) sb += "-p" + config . config . port . toString ( 36 ) ; if ( f !== 0 ) sb += "-f" + f . toString ( 36 ) ; const url = new URL ( < any > config . url ) ; url . search = sb ; ui . ui . panels . master . inviteLink . value = url . toString ( ) ; } function copyInvite ( ) { ui . ui . panels . master . inviteLink . select ( ) ; document . execCommand ( "copy" ) ; log . pushStatus ( "invite" , "Copied invite link" ) ; setTimeout ( function ( ) { log . popStatus ( "invite" ) ; } , 3000 ) ; } function updateCreditCost ( ) { const masterUI = ui . ui . panels . master ; if ( ! credits . creditCost || ! credits . creditRate ) return ; const cc = credits . creditCost ; const cr = credits . creditRate ; if ( net . mode === prot . mode . rec ) cr [ 0 ] += cr [ 1 ] ; masterUI . recordingCost . value = creditsToDollars ( cr [ 0 ] , cc ) ; masterUI . recordingRate . value = creditsToDollars ( cr [ 1 ] * 60 , cc ) + "/hour" ; } function creditsToDollars ( c , creditCost ) { c = Math . ceil ( c * creditCost . currency / creditCost . credits ) ; if ( c === 0 ) return "-" ; else if ( c < 100 ) return c + "¢" ; const d = Math . floor ( c / 100 ) ; let ce = ( c % 100 ) + "" ; if ( ce === "0" ) { return "$" + d ; } else { if ( ce . length === 1 ) ce = "0" + ce ; return "$" + d + "." + ce ; } } export function updateMasterAdmin ( ) { const userAdminP = ui . ui . panels . userAdmin ; const bs = userAdminP . buttons ; for ( let i = 0 ; i < users . length ; i ++ ) { const u = users [ i ] ; if ( ! u ) continue ; let b ; if ( ! bs [ i ] ) { b = bs [ i ] = dce ( "button" ) ; b . classList . add ( "row" ) ; b . innerText = u . name ; b . onclick = ( function ( i ) { return function ( ) { userAdmin ( i ) ; } ; } ) ( i ) ; userAdminP . wrapper . appendChild ( b ) ; } b = bs [ i ] ; if ( u . online ) { b . classList . remove ( "off" ) ; b . disabled = false ; } else { b . classList . add ( "off" ) ; b . disabled = true ; } } } export function userAdmin ( target ) { const user = users [ target ] || null ; const fullAccess = user ? user . fullAccess : null ; const userAdminFull = ui . ui . panels . userAdminFull ; const userAdminUser = fullAccess ? userAdminFull : ui . ui . panels . userAdminUser ; const acts = prot . flags . admin . actions ; userAdminUser . user = target ; if ( target >= 0 ) { userAdminUser . name . innerText = user ? user . name : "" ; userAdminUser . kick . style . display = "" ; } else { userAdminUser . name . innerHTML = '<i class="fas fa-users"></i> All users' ; userAdminUser . kick . style . display = "none" ; } userAdminUser . kick . onclick = function ( ) { adminAction ( target , acts . kick ) ; } ; if ( fullAccess ) { userAdminFull . mute . onchange = function ( ) { userAdminFull . mute . disabled = true ; if ( userAdminFull . mute . checked ) adminAction ( target , acts . mute , { nohide : true } ) ; else adminAction ( target , acts . unmute , { nohide : true } ) ; fullAccess . mute = userAdminFull . mute . checked ; } ; userAdminFull . mute . checked = fullAccess . mute ; userAdminFull . mute . disabled = false ; userAdminFull . echo . onchange = function ( ) { userAdminFull . echo . disabled = true ; if ( userAdminFull . echo . checked ) adminAction ( target , acts . echoCancel , { nohide : true } ) ; else adminAction ( target , acts . unechoCancel , { nohide : true } ) ; fullAccess . echo = userAdminFull . echo . checked ; } ; userAdminFull . echo . checked = fullAccess . echo ; userAdminFull . echo . disabled = false ; userAdminFull . vadSensitivity . onchange = function ( ) { adminAction ( target , acts . vadSensitivity , { nohide : true , arg : + userAdminFull . vadSensitivity . value } ) ; fullAccess . vadSensitivity = + userAdminFull . vadSensitivity . value ; } ; userAdminFull . vadSensitivity . oninput = function ( ) { userAdminFull . vadSensitivityStatus . innerHTML = "&nbsp;" + userAdminFull . vadSensitivity . value ; } ; userAdminFull . vadSensitivity . value = fullAccess . vadSensitivity ; userAdminFull . vadSensitivity . oninput ( null ) ; userAdminFull . vadNoiseGate . onchange = function ( ) { adminAction ( target , acts . vadNoiseGate , { nohide : true , arg : + userAdminFull . vadNoiseGate . value } ) ; fullAccess . vadNoiseGate = + userAdminFull . vadNoiseGate . value ; } ; userAdminFull . vadNoiseGate . oninput = function ( ) { userAdminFull . vadNoiseGateStatus . innerHTML = "&nbsp;" + userAdminFull . vadNoiseGate . value + "dB" ; } ; userAdminFull . vadNoiseGate . value = fullAccess . vadNoiseGate ; userAdminFull . vadNoiseGate . oninput ( null ) ; userAdminFull . vadNoiseGate . className = "ecpeak-horizontal-" + target ; const audioInput = userAdminFull . audioInput ; audioInput . onchange = function ( ) { audioInput . disabled = true ; adminAction ( target , acts . audioInput , { nohide : true , arg : audioInput . value } ) ; } ; audioInput . disabled = false ; audioInput . innerHTML = "" ; try { for ( const dev of fullAccess . audioDevices ) { const el = dce ( "option" ) ; el . value = dev . id ; el . innerText = dev . label ; audioInput . appendChild ( el ) ; } audioInput . value = fullAccess . audioDevice ; } catch ( ex ) { } if ( ! fullAccess . videoDevices ) { userAdminFull . videoHider . style . display = "none" ; } else { userAdminFull . videoHider . style . display = "" ; const videoInput = userAdminFull . videoInput ; videoInput . onchange = function ( ) { videoInput . disabled = true ; adminAction ( target , acts . videoInput , { nohide : true , arg : videoInput . value } ) ; } ; videoInput . disabled = false ; videoInput . innerHTML = "" ; const el = dce ( "option" ) ; el . value = "-none" ; el . innerText = "-" ; videoInput . appendChild ( el ) ; try { for ( const dev of fullAccess . videoDevices ) { const el = dce ( "option" ) ; el . value = dev . id ; el . innerText = dev . label ; videoInput . appendChild ( el ) ; } videoInput . value = fullAccess . videoDevice ; } catch ( ex ) { } const videoRes = userAdminFull . videoRes ; videoRes . onchange = function ( ) { videoRes . disabled = true ; adminAction ( target , acts . videoRes , { nohide : true , arg : videoRes . value } ) ; } ; videoRes . value = fullAccess . videoRes ; videoRes . disabled = false ; } } else { userAdminUser . mute . onclick = function ( ) { adminAction ( target , prot . flags . admin . actions . mute ) ; } ; userAdminUser . echo . onclick = function ( ) { adminAction ( target , prot . flags . admin . actions . echoCancel ) ; } ; ui . ui . panels . userAdminUser . reqFull . onclick = function ( ) { log . pushStatus ( "adminRequest" , "Access requested..." ) ; setTimeout ( function ( ) { log . popStatus ( "adminRequest" ) ; } , 3000 ) ; adminAction ( target , prot . flags . admin . actions . request ) ; ui . showPanel ( null , ui . ui . persistent . main ) ; } ; } ui . showPanel ( userAdminUser , null ) ; } ui . ui . masterUserAdmin = userAdmin ; function addSoundButton ( sid , url , name ) { const soundboard = ui . ui . panels . soundboard ; if ( sid in soundboard . sounds ) return ; sounds . url2sid [ url ] = sid ; const b = soundboard . sounds [ sid ] = { b : dce ( "button" ) , i : dce ( "i" ) , n : dce ( "span" ) } ; b . b . classList . add ( "nouppercase" ) ; b . b . id = "ec-sound-" + sid ; b . i . classList . add ( "fas" ) ; b . i . classList . add ( "fa-play" ) ; b . b . appendChild ( b . i ) ; b . n . innerText = " " + name ; b . b . appendChild ( b . n ) ; const spacer = dce ( "span" ) ; spacer . innerHTML = "&nbsp;" ; b . b . onclick = function ( ) { let play = true ; if ( url in ui . ui . sounds . soundboard ) play = ui . ui . sounds . soundboard [ url ] . el . paused ; playStopSound ( b . b , sid , play ) ; } ; soundboard . soundsWrapper . appendChild ( b . b ) ; soundboard . soundsWrapper . appendChild ( spacer ) ; ui . ui . persistent . sounds . style . display = "" ; } function addSoundButtons ( arr ) { arr . forEach ( function ( s ) { addSoundButton ( s . i , s . u , s . n ) ; } ) ; } function playStopSound ( b , sid , play ) { b . disabled = true ; b . classList . add ( "off" ) ; const p = prot . parts . sound . cs ; const sidBuf = util . encodeText ( sid ) ; const msg = new DataView ( new ArrayBuffer ( p . length + sidBuf . length ) ) ; msg . setUint32 ( 0 , prot . ids . sound , true ) ; msg . setUint8 ( p . status , play ? 1 : 0 ) ; new Uint8Array ( msg . buffer ) . set ( sidBuf , p . id ) ; net . masterSock . send ( msg ) ; } function soundButtonUpdate ( url , play , el ) { const soundboard = ui . ui . panels . soundboard ; const sid = sounds . url2sid [ url ] ; if ( ! sid ) return ; const b = soundboard . sounds [ sid ] ; b . b . disabled = false ; b . b . classList . remove ( "off" ) ; b . i . classList . remove ( "fa-play" ) ; b . i . classList . remove ( "fa-stop" ) ; b . i . classList . add ( play ? "fa-stop" : "fa-play" ) ; if ( play ) { el . addEventListener ( "ended" , function ( ) { soundButtonUpdate ( url , false , el ) ; } , { once : true } ) ; } } function adminAction ( target , action , opts ? ) { opts = opts || { } ; const arg = opts . arg || null ; let argBuf = null ; if ( typeof arg === "string" ) { argBuf = util . encodeText ( arg ) ; } else if ( typeof arg === "number" ) { argBuf = new Uint8Array ( 4 ) ; ( new Int32Array ( argBuf . buffer ) ) [ 0 ] = arg ; } else { argBuf = new Uint8Array ( 0 ) ; } const p = prot . parts . admin ; const out = new DataView ( new ArrayBuffer ( p . length + argBuf . length ) ) ; out . setUint32 ( 0 , prot . ids . admin , true ) ; out . setUint32 ( p . target , target , true ) ; out . setUint32 ( p . action , action , true ) ; ( new Uint8Array ( out . buffer ) ) . set ( argBuf , p . argument ) ; net . masterSock . send ( out . buffer ) ; if ( ! opts . nohide ) ui . showPanel ( null , ui . ui . persistent . main ) ; } function acceptRemoteVideoChange ( ) { const arv = ui . ui . panels . master . acceptRemoteVideo ; localStorage . setItem ( "ecmaster-video-record-host" , JSON . stringify ( arv . checked ) ) ; jitsi . videoRecSend ( void 0 , prot . videoRec . videoRecHost , ~ ~ arv . checked ) ; } function allowAdmin ( target , allowed , props ) { if ( ! users [ target ] ) return ; const user = users [ target ] ; const name = user . name || "Anonymous" ; if ( allowed ) { user . fullAccess = props ; log . pushStatus ( "allowAdmin" , "User " + name + " has allowed admin access." ) ; } else { user . fullAccess = null ; log . pushStatus ( "allowAdmin" , "User " + name + " has disallowed admin access." ) ; } setTimeout ( function ( ) { log . popStatus ( "allowAdmin" ) ; } , 5000 ) ; } function updateAdmin ( target , props ) { if ( ! users [ target ] ) return ; const user = users [ target ] ; if ( ! user . fullAccess ) return ; [ "audioDevices" , "audioDevice" , "videoDevices" , "videoDevice" , "videoRes" , "videoRec" , "mute" , "echo" ] . forEach ( prop => { if ( ! ( prop in props ) ) return ; user . fullAccess [ prop ] = props [ prop ] ; } ) ; const userAdminFull = ui . ui . panels . userAdminFull ; if ( userAdminFull . user === target ) { if ( "mute" in props ) { userAdminFull . mute . checked = props . mute ; userAdminFull . mute . disabled = false ; } if ( "echo" in props ) { userAdminFull . echo . checked = props . echo ; userAdminFull . echo . disabled = false ; } if ( "audioDevice" in props ) { userAdminFull . audioInput . value = props . audioDevice ; userAdminFull . audioInput . disabled = false ; } if ( "videoDevice" in props ) { userAdminFull . videoInput . value = props . videoDevice ; userAdminFull . videoInput . disabled = false ; } if ( "videoRes" in props ) { userAdminFull . videoRes . value = props . videoRes ; userAdminFull . videoRes . disabled = false ; } } }